diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 6362336963..0244578094 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -73,10 +73,10 @@ body: required: true - type: input - id: redis-version + id: engine-version attributes: - label: Redis Version - description: E.g. 6.2, 7.0 + label: Engine type and version + description: E.g. Valkey 7.0 validations: required: true @@ -111,7 +111,7 @@ body: attributes: label: Cluster information description: | - Redis cluster information, cluster topology, number of shards, number of replicas, used data types. + Cluster information, cluster topology, number of shards, number of replicas, used data types. validations: required: false @@ -120,7 +120,7 @@ body: attributes: label: Logs description: | - Client and/or Redis logs. + Client and/or server logs. validations: required: false diff --git a/.github/json_matrices/build-matrix.json b/.github/json_matrices/build-matrix.json new file mode 100644 index 0000000000..e8589b84f8 --- /dev/null +++ b/.github/json_matrices/build-matrix.json @@ -0,0 +1,55 @@ +[ + { + "OS": "ubuntu", + "NAMED_OS": "linux", + "RUNNER": "ubuntu-latest", + "ARCH": "x64", + "TARGET": "x86_64-unknown-linux-gnu", + "PACKAGE_MANAGERS": ["pypi", "npm"] + }, + { + "OS": "ubuntu", + "NAMED_OS": "linux", + "RUNNER": ["self-hosted", "Linux", "ARM64"], + "ARCH": "arm64", + "TARGET": "aarch64-unknown-linux-gnu", + "PACKAGE_MANAGERS": ["pypi", "npm"], + "CONTAINER": "2_28" + }, + { + "OS": "macos", + "NAMED_OS": "darwin", + "RUNNER": "macos-12", + "ARCH": "x64", + "TARGET": "x86_64-apple-darwin", + "PACKAGE_MANAGERS": ["pypi", "npm"] + }, + { + "OS": "macos", + "NAMED_OS": "darwin", + "RUNNER": "macos-latest", + "ARCH": "arm64", + "TARGET": "aarch64-apple-darwin", + "PACKAGE_MANAGERS": ["pypi", "npm"] + }, + { + "OS": "ubuntu", + "NAMED_OS": "linux", + "ARCH": "arm64", + "TARGET": "aarch64-unknown-linux-musl", + "RUNNER": ["self-hosted", "Linux", "ARM64"], + "IMAGE": "node:alpine", + "CONTAINER_OPTIONS": "--user root --privileged --rm", + "PACKAGE_MANAGERS": ["npm"] + }, + { + "OS": "ubuntu", + "NAMED_OS": "linux", + "ARCH": "x64", + "TARGET": "x86_64-unknown-linux-musl", + "RUNNER": "ubuntu-latest", + "IMAGE": "node:alpine", + "CONTAINER_OPTIONS": "--user root --privileged", + "PACKAGE_MANAGERS": ["npm"] + } +] diff --git a/.github/json_matrices/engine-matrix.json b/.github/json_matrices/engine-matrix.json new file mode 100644 index 0000000000..f20f0c955e --- /dev/null +++ b/.github/json_matrices/engine-matrix.json @@ -0,0 +1,6 @@ +[ + { + "type": "valkey", + "version": "7.2.5" + } +] diff --git a/.github/workflows/build-node-wrapper/action.yml b/.github/workflows/build-node-wrapper/action.yml index 0ec2e0d1c3..98246df22f 100644 --- a/.github/workflows/build-node-wrapper/action.yml +++ b/.github/workflows/build-node-wrapper/action.yml @@ -7,8 +7,8 @@ inputs: type: string options: - amazon-linux - - macos-latest - - ubuntu-latest + - macos + - ubuntu named_os: description: "The name of the current operating system" required: false @@ -29,6 +29,10 @@ inputs: description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" type: string required: true + engine-version: + description: "Engine version to install" + required: true + type: string publish: description: "Enable building the wrapper in release mode" required: false @@ -38,7 +42,7 @@ inputs: description: "The NPM scope" required: false type: string - default: "@aws" + default: "@valkey" github-token: description: "GITHUB_TOKEN, GitHub App installation access token" required: true @@ -56,6 +60,7 @@ runs: os: ${{ inputs.os }} target: ${{ inputs.target }} github-token: ${{ inputs.github-token }} + engine-version: ${{ inputs.engine-version }} - name: Create package.json file uses: ./.github/workflows/node-create-package-file @@ -65,14 +70,15 @@ runs: named_os: ${{ inputs.named_os }} arch: ${{ inputs.arch }} npm_scope: ${{ inputs.npm_scope }} - + target: ${{ inputs.target }} + - name: npm install shell: bash working-directory: ./node run: | rm -rf node_modules && npm install --frozen-lockfile cd rust-client - npm install + npm install --frozen-lockfile - name: Build shell: bash diff --git a/.github/workflows/build-python-wrapper/action.yml b/.github/workflows/build-python-wrapper/action.yml index bfb2b28564..72863c6a43 100644 --- a/.github/workflows/build-python-wrapper/action.yml +++ b/.github/workflows/build-python-wrapper/action.yml @@ -7,12 +7,16 @@ inputs: type: string options: - amazon-linux - - macos-latest - - ubuntu-latest + - macos + - ubuntu target: description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" type: string required: true + engine-version: + description: "Engine version to install" + required: true + type: string publish: description: "Enable building the wrapper in release mode" required: false @@ -34,13 +38,16 @@ runs: os: ${{ inputs.os }} target: ${{ inputs.target }} github-token: ${{ inputs.github-token }} + engine-version: ${{ inputs.engine-version }} - name: Install Python software dependencies shell: bash run: | + # Disregarding PEP 668 as it addresses package managers conflicts, which is not applicable in the CI scope. + INSTALL_FLAGS=`if [[ "${{ inputs.os }}" =~ .*"macos".* ]]; then echo "--break-system-packages"; else echo ""; fi` python3 -m ensurepip --upgrade || true - python3 -m pip install --upgrade pip - python3 -m pip install virtualenv mypy-protobuf + python3 -m pip install --upgrade pip $INSTALL_FLAGS + python3 -m pip install virtualenv mypy-protobuf $INSTALL_FLAGS - name: Generate protobuf files shell: bash @@ -58,6 +65,5 @@ runs: source "$HOME/.cargo/env" python3 -m venv .env source .env/bin/activate - python3 -m pip install --upgrade pip python3 -m pip install --no-cache-dir -r requirements.txt maturin develop diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index 5239f3b9d7..36b380c3e0 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -9,9 +9,10 @@ on: - submodules/** - .github/workflows/csharp.yml - .github/workflows/install-shared-dependencies/action.yml - - .github/workflows/install-redis/action.yml - .github/workflows/test-benchmark/action.yml - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json pull_request: paths: - csharp/** @@ -19,9 +20,11 @@ on: - submodules/** - .github/workflows/csharp.yml - .github/workflows/install-shared-dependencies/action.yml - - .github/workflows/install-redis/action.yml - .github/workflows/test-benchmark/action.yml - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json + workflow_dispatch: permissions: contents: read @@ -31,45 +34,60 @@ concurrency: cancel-in-progress: true jobs: + load-engine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + run-tests: + needs: load-engine-matrix timeout-minutes: 25 strategy: fail-fast: false matrix: - redis: - - 6.2.14 - - 7.2.3 + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} dotnet: - - '6.0' + # - '6.0' - '8.0' - os: - - ubuntu-latest - - macos-latest - runs-on: ${{ matrix.os }} + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + # - { + # OS: macos, + # RUNNER: macos-latest, + # TARGET: aarch64-apple-darwin + # } + + runs-on: ${{ matrix.host.RUNNER }} steps: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install redis - # TODO: make this step macos compatible: https://github.com/aws/glide-for-redis/issues/781 - if: ${{ matrix.os == 'ubuntu-latest' }} - uses: ./.github/workflows/install-redis + - name: Set up dotnet ${{ matrix.dotnet }} + uses: actions/setup-dotnet@v4 with: - redis-version: ${{ matrix.redis }} - + dotnet-version: ${{ matrix.dotnet }} + - name: Install shared software dependencies uses: ./.github/workflows/install-shared-dependencies with: - os: ${{ matrix.os }} - target: ${{ matrix.os == 'ubuntu-latest' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }} + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up dotnet ${{ matrix.dotnet }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ matrix.dotnet }} + engine-version: ${{ matrix.engine.version }} - name: Format working-directory: ./csharp @@ -81,19 +99,21 @@ jobs: - uses: ./.github/workflows/test-benchmark with: - language-flag: -csharp + language-flag: -csharp -dotnet-framework net${{ matrix.dotnet }} - name: Upload test reports if: always() continue-on-error: true uses: actions/upload-artifact@v4 with: - name: test-reports-dotnet-${{ matrix.dotnet }}-redis-${{ matrix.redis }}-${{ matrix.os }} + name: test-reports-dotnet-${{ matrix.dotnet }}-redis-${{ matrix.redis }}-${{ matrix.host.RUNNER }} path: | csharp/TestReport.html benchmarks/results/* utils/clusters/** +# TODO Add amazonlinux + lint-rust: timeout-minutes: 10 runs-on: ubuntu-latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 805c46ba6c..a7654519d7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -8,36 +8,66 @@ on: - submodules/** - go/** - .github/workflows/go.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json pull_request: paths: - glide-core/src/** - submodules/** - go/** - .github/workflows/go.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: go-${{ github.head_ref || github.ref }} cancel-in-progress: true - jobs: + load-engine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + build-and-test-go-client: + needs: load-engine-matrix timeout-minutes: 35 strategy: # Run all jobs fail-fast: false matrix: go: - - '1.18.10' + # - '1.18.10' - '1.22.0' - redis: - - 6.2.14 - - 7.2.3 - os: - - ubuntu-latest - - macos-latest - - runs-on: ${{ matrix.os }} + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + # - { + # OS: macos, + # RUNNER: macos-latest, + # TARGET: aarch64-apple-darwin + # } + + runs-on: ${{ matrix.host.RUNNER }} steps: - uses: actions/checkout@v4 @@ -53,16 +83,10 @@ jobs: - name: Install shared software dependencies uses: ./.github/workflows/install-shared-dependencies with: - os: ${{ matrix.os }} - target: ${{ matrix.os == 'ubuntu-latest' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }} + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install redis - # TODO: make this step macos compatible: https://github.com/aws/glide-for-redis/issues/781 - if: ${{ matrix.os == 'ubuntu-latest' }} - uses: ./.github/workflows/install-redis - with: - redis-version: ${{ matrix.redis }} + engine-version: ${{ matrix.engine.version }} - name: Install tools for Go ${{ matrix.go }} working-directory: ./go @@ -85,7 +109,7 @@ jobs: run: make test build-amazonlinux-latest: - if: github.repository_owner == 'aws' + if: github.repository_owner == 'valkey-io' strategy: # Run all jobs fail-fast: false @@ -119,11 +143,7 @@ jobs: os: "amazon-linux" target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Create a symbolic Link for redis6 binaries - run: | - ln -s /usr/bin/redis6-server /usr/bin/redis-server - ln -s /usr/bin/redis6-cli /usr/bin/redis-cli + engine-version: "7.2.5" - name: Install Go run: | diff --git a/.github/workflows/install-redis-modules/action.yml b/.github/workflows/install-redis-modules/action.yml deleted file mode 100644 index e4e9c9453a..0000000000 --- a/.github/workflows/install-redis-modules/action.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Install Redis Modules - -inputs: - redis-version: - description: "redis version of clusters" - required: true - type: string - - modules: - description: "required redis modules to install" - required: false - type: string - default: 'all' - options: - - "all" - - "search" - - "json" - - - -runs: - using: "composite" - steps: - - name: Cache RedisJSON Dependencies - if: inputs.modules == 'all' || inputs.modules == 'json' - id: cache-dependencies-redisjson - uses: actions/cache@v3 - with: - path: | - ./cmake - ./redisjson/bin - key: ${{ runner.os }}-${{ inputs.redis-version }}-redisjson - - - - name: Install CMake - if: steps.cache-dependencies-redisearch.outputs.cache-hit != 'true' || steps.cache-dependencies-redisjson.outputs.cache-hit != 'true' - shell: bash - run: | - set -x - sudo apt-get update - sudo apt-get install -y cmake - cp /usr/bin/cmake ./cmake - - - - name: Checkout RedisJSON Repository - if: steps.cache-dependencies-redisjson.outputs.cache-hit != 'true' && (inputs.modules == 'all' || inputs.modules == 'json') - uses: actions/checkout@v4 - with: - repository: "RedisJSON/RedisJSON" - path: "./redisjson" - ref: ${{ startsWith(inputs.redis-version, '6') && 'v2.6.0' || '' }} - submodules: recursive - - - name: Build RedisJSON - if: steps.cache-dependencies-redisjson.outputs.cache-hit != 'true' && (inputs.modules == 'all' || inputs.modules == 'json') - shell: bash - working-directory: ./redisjson - run: | - set -x - echo "Building RedisJSON..." - make - - - name: Copy redisjson.so - if: inputs.modules == 'all' || inputs.modules == 'json' - shell: bash - run: | - set -x - echo "Copying RedisJSON..." - cp $GITHUB_WORKSPACE/redisjson/bin/linux-x64-release/rejson.so $GITHUB_WORKSPACE/redisjson.so diff --git a/.github/workflows/install-rust-and-protoc/action.yml b/.github/workflows/install-rust-and-protoc/action.yml new file mode 100644 index 0000000000..e1222ffd9d --- /dev/null +++ b/.github/workflows/install-rust-and-protoc/action.yml @@ -0,0 +1,32 @@ +name: Install Rust tool chain and protoc + +inputs: + target: + description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" + type: string + required: false + default: "x86_64-unknown-linux-gnu" + options: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - x86_64-apple-darwin + - aarch64-apple-darwin + github-token: + description: "GitHub token" + type: string + required: true + + +runs: + using: "composite" + steps: + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ inputs.target }} + + - name: Install protoc (protobuf) + uses: arduino/setup-protoc@v3 + with: + version: "25.1" + repo-token: ${{ inputs.github-token }} diff --git a/.github/workflows/install-shared-dependencies/action.yml b/.github/workflows/install-shared-dependencies/action.yml index 42747ac0be..af44e7206f 100644 --- a/.github/workflows/install-shared-dependencies/action.yml +++ b/.github/workflows/install-shared-dependencies/action.yml @@ -7,8 +7,8 @@ inputs: type: string options: - amazon-linux - - macos-latest - - ubuntu-latest + - macos + - ubuntu target: description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" type: string @@ -19,42 +19,61 @@ inputs: - aarch64-unknown-linux-gnu - x86_64-apple-darwin - aarch64-apple-darwin + - aarch64-unknown-linux-musl + - x86_64-unknown-linux-musl + engine-version: + description: "Engine version to install" + required: true + type: string + github-token: description: "GITHUB_TOKEN, GitHub App installation access token" required: true type: string + runs: using: "composite" steps: - name: Install software dependencies for macOS shell: bash - if: "${{ inputs.os == 'macos-latest' }}" + if: "${{ inputs.os == 'macos' }}" run: | brew update brew upgrade || true - brew install git gcc pkgconfig openssl redis coreutils + brew install git gcc pkgconfig openssl coreutils - - name: Install software dependencies for Ubuntu + - name: Install software dependencies for Ubuntu GNU shell: bash - if: "${{ inputs.os == 'ubuntu-latest' }}" + if: "${{ inputs.os == 'ubuntu' && !contains(inputs.target, 'musl')}}" run: | sudo apt update -y sudo apt install -y git gcc pkg-config openssl libssl-dev + + - name: Install software dependencies for Ubuntu MUSL + shell: bash + if: "${{ contains(inputs.target, 'musl') }}" + run: | + apk update + wget -O - https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" + apk add protobuf-dev musl-dev make gcc envsubst openssl libressl-dev - name: Install software dependencies for Amazon-Linux shell: bash if: "${{ inputs.os == 'amazon-linux' }}" run: | - yum install -y gcc pkgconfig openssl openssl-devel which curl redis6 gettext --allowerasing + yum install -y gcc pkgconfig openssl openssl-devel which curl gettext --allowerasing - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + - name: Install Rust toolchain and protoc + if: "${{ !contains(inputs.target, 'musl') }}" + uses: ./.github/workflows/install-rust-and-protoc with: - targets: ${{ inputs.target }} + target: ${{ inputs.target }} + github-token: ${{ inputs.github-token }} - - name: Install protoc (protobuf) - uses: arduino/setup-protoc@v3 + - name: Install Valkey + uses: ./.github/workflows/install-valkey with: - version: "25.1" - repo-token: ${{ inputs.github-token }} + engine-version: ${{ inputs.engine-version }} + target: ${{ inputs.target }} diff --git a/.github/workflows/install-valkey/action.yml b/.github/workflows/install-valkey/action.yml new file mode 100644 index 0000000000..f15a875c03 --- /dev/null +++ b/.github/workflows/install-valkey/action.yml @@ -0,0 +1,78 @@ +name: Install Valkey + +inputs: + engine-version: + description: "Engine version to install" + required: true + type: string + target: + description: "Specified target toolchain, ex. x86_64-unknown-linux-gnu" + type: string + required: true + options: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - x86_64-apple-darwin + - aarch64-apple-darwin + - aarch64-unknown-linux-musl + - x86_64-unknown-linux-musl + +env: + CARGO_TERM_COLOR: always + VALKEY_MIN_VERSION: "7.2.5" + +runs: + using: "composite" + + steps: + - name: Cache Valkey + # TODO: remove the musl ARM64 limitation when https://github.com/actions/runner/issues/801 is resolved + if: ${{ inputs.target != 'aarch64-unknown-linux-musl' }} + uses: actions/cache@v4 + id: cache-valkey + with: + path: | + ~/valkey + key: valkey-${{ inputs.engine-version }}-${{ inputs.target }} + + - name: Build Valkey + if: ${{ steps.cache-valkey.outputs.cache-hit != 'true' }} + shell: bash + run: | + echo "Building valkey ${{ inputs.engine-version }}" + cd ~ + rm -rf valkey + git clone https://github.com/valkey-io/valkey.git + cd valkey + git checkout ${{ inputs.engine-version }} + make BUILD_TLS=yes + + - name: Install Valkey + shell: bash + run: | + cd ~/valkey + if command -v sudo &> /dev/null + then + echo "sudo command exists" + sudo make install + else + echo "sudo command does not exist" + make install + fi + echo 'export PATH=/usr/local/bin:$PATH' >>~/.bash_profile + + - name: Verify Valkey installation and symlinks + shell: bash + run: | + # In Valkey releases, the engine is built with symlinks from valkey-server and valkey-cli + # to redis-server and redis-cli. This step ensures that the engine is properly installed + # with the expected version and that Valkey symlinks are correctly created. + EXPECTED_VERSION=`echo ${{ inputs.engine-version }} | sed -e "s/^redis-//"` + INSTALLED_VER=$(redis-server -v) + if [[ $INSTALLED_VER != *"${EXPECTED_VERSION}"* ]]; then + echo "Wrong version has been installed. Expected: $EXPECTED_VERSION, Installed: $INSTALLED_VER" + exit 1 + else + echo "Successfully installed the server: $INSTALLED_VER" + fi + diff --git a/.github/workflows/java-cd.yml b/.github/workflows/java-cd.yml new file mode 100644 index 0000000000..0859892b45 --- /dev/null +++ b/.github/workflows/java-cd.yml @@ -0,0 +1,160 @@ +name: Java Prepare Deployment + +on: + pull_request: + paths: + - .github/workflows/java-cd.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/start-self-hosted-runner/action.yml + - .github/json_matrices/build-matrix.json + push: + tags: + - "v*.*" + workflow_dispatch: + inputs: + version: + description: 'The release version of GLIDE, formatted as *.*.* or *.*.*-rc*' + required: true + +concurrency: + group: java-cd-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +permissions: + id-token: write + +jobs: + start-self-hosted-runner: + if: github.repository_owner == 'valkey-io' + runs-on: ubuntu-latest + environment: AWS_ACTIONS + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Start self hosted EC2 runner + uses: ./.github/workflows/start-self-hosted-runner + with: + role-to-assume: ${{ secrets.ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + ec2-instance-id: ${{ secrets.AWS_EC2_INSTANCE_ID }} + + create-binaries-to-publish: + needs: start-self-hosted-runner + if: github.repository_owner == 'valkey-io' + timeout-minutes: 35 + env: + JAVA_VERSION: '11' + strategy: + # Run all jobs + fail-fast: false + matrix: + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu, + } + - { + OS: ubuntu, + RUNNER: ["self-hosted", "Linux", "ARM64"], + TARGET: aarch64-unknown-linux-gnu, + } + - { + OS: macos, + RUNNER: macos-12, + TARGET: x86_64-apple-darwin, + } + - { + OS: macos, + RUNNER: macos-latest, + TARGET: aarch64-apple-darwin, + } + + runs-on: ${{ matrix.host.RUNNER }} + + steps: + - name: Setup self-hosted runner access + run: | + GHA_HOME=/home/ubuntu/actions-runner/_work/valkey-glide + if [ -d $GHA_HOME ]; then + sudo chown -R $USER:$USER $GHA_HOME + sudo rm -rf $GHA_HOME + mkdir -p $GHA_HOME/valkey-glide + else + echo "No cleaning needed" + fi + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set the release version + shell: bash + run: | + if ${{ github.event_name == 'pull_request' }}; then + R_VERSION="255.255.255" + elif ${{ github.event_name == 'workflow_dispatch' }}; then + R_VERSION="${{ env.INPUT_VERSION }}" + else + R_VERSION=${GITHUB_REF:11} + fi + echo "RELEASE_VERSION=${R_VERSION}" >> $GITHUB_ENV + echo "Release version detected: $R_VERSION" + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ github.event.inputs.version }} + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Install shared software dependencies + uses: ./.github/workflows/install-shared-dependencies + with: + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install protoc (protobuf) + uses: arduino/setup-protoc@v3 + with: + version: "26.1" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create secret key ring file + working-directory: java/client + run: | + echo "$SECRING_GPG" | base64 --decode > ./secring.gpg + ls -ltr + env: + SECRING_GPG: ${{ secrets.SECRING_GPG }} + + - name: Build java client + working-directory: java + run: | + ./gradlew :client:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} + env: + GLIDE_RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + + - name: Bundle JAR + working-directory: java + run: | + src_folder=~/.m2/repository/io/valkey/valkey-glide/${{ env.RELEASE_VERSION }} + cd $src_folder + jar -cvf bundle.jar * + ls -ltr + cd - + cp $src_folder/bundle.jar bundle-${{ matrix.host.TARGET }}.jar + + - name: Upload artifacts to publish + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: java-${{ matrix.host.TARGET }} + path: | + java/bundle*.jar diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index c7e57b89dc..5106aec4ce 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -9,9 +9,10 @@ on: - java/** - .github/workflows/java.yml - .github/workflows/install-shared-dependencies/action.yml - - .github/workflows/install-redis/action.yml - .github/workflows/test-benchmark/action.yml - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json pull_request: paths: - glide-core/src/** @@ -19,38 +20,62 @@ on: - java/** - .github/workflows/java.yml - .github/workflows/install-shared-dependencies/action.yml - - .github/workflows/install-redis/action.yml - .github/workflows/test-benchmark/action.yml - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: java-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: + load-engine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + build-and-test-java-client: + needs: load-engine-matrix timeout-minutes: 35 strategy: # Run all jobs fail-fast: false matrix: java: - - 11 + # - 11 - 17 - redis: - - 6.2.14 - - 7.2.3 - os: - - ubuntu-latest - - macos-latest - - runs-on: ${{ matrix.os }} + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + # - { + # OS: macos, + # RUNNER: macos-latest, + # TARGET: aarch64-apple-darwin + # } + + runs-on: ${{ matrix.host.RUNNER }} steps: - uses: actions/checkout@v4 with: submodules: recursive + - uses: gradle/actions/wrapper-validation@v3 + - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v4 with: @@ -60,9 +85,10 @@ jobs: - name: Install shared software dependencies uses: ./.github/workflows/install-shared-dependencies with: - os: ${{ matrix.os }} - target: ${{ matrix.os == 'ubuntu-latest' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }} + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: ${{ matrix.engine.version }} - name: Install protoc (protobuf) uses: arduino/setup-protoc@v3 @@ -70,16 +96,9 @@ jobs: version: "26.1" repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install redis - # TODO: make this step macos compatible: https://github.com/aws/glide-for-redis/issues/781 - if: ${{ matrix.os == 'ubuntu-latest' }} - uses: ./.github/workflows/install-redis - with: - redis-version: ${{ matrix.redis }} - - name: Build java client working-directory: java - run: ./gradlew --continue build + run: ./gradlew --continue build -x javadoc - name: Ensure no skipped files by linter working-directory: java @@ -89,30 +108,31 @@ jobs: with: language-flag: -java - - name: Upload test reports + - name: Upload test & spotbugs reports if: always() continue-on-error: true uses: actions/upload-artifact@v4 with: - name: test-reports-java-${{ matrix.java }}-redis-${{ matrix.redis }}-${{ matrix.os }} + name: test-reports-java-${{ matrix.java }}-${{ matrix.engine.type }}-${{ matrix.engine.version }}-${{ matrix.host.RUNNER }} path: | java/client/build/reports/** java/integTest/build/reports/** utils/clusters/** benchmarks/results/** + java/client/build/reports/spotbugs/** build-amazonlinux-latest: - if: github.repository_owner == 'aws' + if: github.repository_owner == 'valkey-io' strategy: # Run all jobs fail-fast: false matrix: java: - - 11 + # - 11 - 17 runs-on: ubuntu-latest container: amazonlinux:latest - timeout-minutes: 15 + timeout-minutes: 35 steps: - name: Install git run: | @@ -136,6 +156,7 @@ jobs: os: "amazon-linux" target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: "7.2.5" - name: Install protoc (protobuf) uses: arduino/setup-protoc@v3 @@ -143,32 +164,24 @@ jobs: version: "26.1" repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Create a symbolic Link for redis6 binaries - run: | - ln -s /usr/bin/redis6-server /usr/bin/redis-server - ln -s /usr/bin/redis6-cli /usr/bin/redis-cli - - name: Install Java run: | - yum install -y java-${{ matrix.java }} - - - name: Build rust part - working-directory: java - run: cargo build --release + yum install -y java-${{ matrix.java }}-amazon-corretto-devel.x86_64 - - name: Build java part + - name: Build java wrapper working-directory: java - run: ./gradlew --continue build + run: ./gradlew --continue build -x javadoc - - name: Upload test reports + - name: Upload test & spotbugs reports if: always() continue-on-error: true uses: actions/upload-artifact@v4 with: - name: test-reports-${{ matrix.java }} + name: test-reports-${{ matrix.java }}-amazon-linux path: | java/client/build/reports/** java/integTest/build/reports/** + java/client/build/reports/spotbugs/** lint-rust: timeout-minutes: 15 diff --git a/.github/workflows/lint-rust/action.yml b/.github/workflows/lint-rust/action.yml index ec1531e877..8a7cdf185f 100644 --- a/.github/workflows/lint-rust/action.yml +++ b/.github/workflows/lint-rust/action.yml @@ -5,6 +5,10 @@ inputs: description: "folder that contains the target Cargo.toml file" required: true type: string + github-token: + description: "github token" + required: false + type: string runs: using: "composite" @@ -14,13 +18,14 @@ runs: with: submodules: recursive - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - name: Install Rust toolchain and protoc + uses: ./.github/workflows/install-rust-and-protoc + with: + github-token: ${{ inputs.github-token }} - - name: Install protoc (protobuf) - uses: arduino/setup-protoc@v3 + - uses: Swatinem/rust-cache@v2 with: - version: "25.1" + github-token: ${{ inputs.github-token }} - run: cargo fmt --all -- --check working-directory: ${{ inputs.cargo-toml-folder }} diff --git a/.github/workflows/node-create-package-file/action.yml b/.github/workflows/node-create-package-file/action.yml index bdadeeb8ba..1da7510aab 100644 --- a/.github/workflows/node-create-package-file/action.yml +++ b/.github/workflows/node-create-package-file/action.yml @@ -11,8 +11,8 @@ inputs: type: string options: - amazon-linux - - macos-latest - - ubuntu-latest + - macos + - ubuntu named_os: description: "The name of the current operating system" required: false @@ -33,7 +33,18 @@ inputs: description: "The NPM scope" required: false type: string - default: "@aws" + default: "@valkey" + target: + description: "Specified target for rust toolchain, ex. x86_64-apple-darwin" + type: string + required: true + options: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - x86_64-apple-darwin + - aarch64-apple-darwin + - aarch64-unknown-linux-musl + - x86_64-unknown-linux-musl runs: using: "composite" @@ -42,25 +53,35 @@ runs: shell: bash working-directory: ./node run: | + # echo -musl if inputs.target is musl + export MUSL_FLAG=`if [[ "${{ inputs.target }}" =~ .*"musl".* ]]; then echo "-musl"; fi` # set the package name - name="glide-for-redis" + name="valkey-glide" # derive the OS and architecture from the inputs export node_os="${{ inputs.named_os }}" export node_arch="${{ inputs.arch }}" # set the version export package_version="${{ inputs.release_version }}" # set the package name - export pkg_name="${name}-${node_os}-${node_arch}" + export pkg_name="${name}-${node_os}${MUSL_FLAG}-${node_arch}" # set the scope export scope=`if [ "${{ inputs.npm_scope }}" != '' ]; then echo "${{ inputs.npm_scope }}/"; fi` # set the registry scope export registry_scope=`if [ "${{ inputs.npm_scope }}" != '' ]; then echo "${{ inputs.npm_scope }}:"; fi` # remove the current name section - SED_FOR_MACOS=`if [[ "${{ inputs.os }}" =~ .*"macos".* ]]; then echo "''"; fi` - sed -i $SED_FOR_MACOS '/"name":/d' ./package.json + if [[ "${{ inputs.os }}" =~ .*"macos".* ]]; then + sed '/"name":/d' ./package.json > temp.json && mv temp.json package.json + else + sed -i '/"name":/d' ./package.json + fi # Remove all `///` occurrences to enable the commented out sections - sed -i -e 's|///||g' package.json + if [[ "${{ inputs.os }}" =~ .*"macos".* ]]; then + sed 's|///||g' package.json > temp.json && mv temp.json package.json + else + sed -i 's|///||g' package.json + fi # generate package.json from the template mv package.json package.json.tmpl envsubst < package.json.tmpl > "package.json" cat package.json + echo $(ls *json*) diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 4248e4fc81..5981ad2007 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -11,6 +11,10 @@ on: - .github/workflows/node.yml - .github/workflows/build-node-wrapper/action.yml - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json pull_request: paths: - glide-core/src/** @@ -20,6 +24,11 @@ on: - .github/workflows/node.yml - .github/workflows/build-node-wrapper/action.yml - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: node-${{ github.head_ref || github.ref }} @@ -29,26 +38,33 @@ env: CARGO_TERM_COLOR: always jobs: + load-engine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + test-ubuntu-latest: runs-on: ubuntu-latest + needs: load-engine-matrix timeout-minutes: 15 strategy: fail-fast: false matrix: - redis: - - 6.2.14 - - 7.2.3 - + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + steps: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install redis - uses: ./.github/workflows/install-redis - with: - redis-version: ${{ matrix.redis }} - - name: Use Node.js 16.x uses: actions/setup-node@v3 with: @@ -57,14 +73,10 @@ jobs: - name: Build Node wrapper uses: ./.github/workflows/build-node-wrapper with: - os: "ubuntu-latest" + os: "ubuntu" target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Redis Modules - uses: ./.github/workflows/install-redis-modules - with: - redis-version: ${{ matrix.redis }} + engine-version: ${{ matrix.engine.version }} - name: test run: npm test @@ -83,10 +95,6 @@ jobs: npm ci npm run build-and-test working-directory: ./node/hybrid-node-tests/ecmascript-test - - - name: test redis modules - run: npm run test-modules -- --load-module=$GITHUB_WORKSPACE/redisjson.so - working-directory: ./node - uses: ./.github/workflows/test-benchmark with: @@ -105,37 +113,39 @@ jobs: cargo-toml-folder: ./node/rust-client name: lint node rust - build-macos-latest: - runs-on: macos-latest - timeout-minutes: 25 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Set up Homebrew - uses: Homebrew/actions/setup-homebrew@master - - - name: Install NodeJS - run: | - brew update - brew upgrade || true - brew install node - - - name: Downgrade npm major version to 8 - run: | - npm i -g npm@8 - - - name: Build Node wrapper - uses: ./.github/workflows/build-node-wrapper - with: - os: "macos-latest" - named_os: "darwin" - target: "x86_64-apple-darwin" - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Test compatibility - run: npm test -- -t "set and get flow works" - working-directory: ./node + # build-macos-latest: + # runs-on: macos-latest + # timeout-minutes: 25 + # steps: + # - uses: actions/checkout@v4 + # with: + # submodules: recursive + # - name: Set up Homebrew + # uses: Homebrew/actions/setup-homebrew@master + + # - name: Install NodeJS + # run: | + # brew update + # brew upgrade || true + # brew install node + + # - name: Downgrade npm major version to 8 + # run: | + # npm i -g npm@8 + + # - name: Build Node wrapper + # uses: ./.github/workflows/build-node-wrapper + # with: + # os: "macos" + # named_os: "darwin" + # arch: "arm64" + # target: "aarch64-apple-darwin" + # github-token: ${{ secrets.GITHUB_TOKEN }} + # engine-version: "7.2.5" + + # - name: Test compatibility + # run: npm test -- -t "set and get flow works" + # working-directory: ./node build-amazonlinux-latest: runs-on: ubuntu-latest @@ -167,13 +177,47 @@ jobs: os: "amazon-linux" target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Create a symbolic Link for redis6 binaries - working-directory: ./node - run: | - ln -s /usr/bin/redis6-server /usr/bin/redis-server - ln -s /usr/bin/redis6-cli /usr/bin/redis-cli + engine-version: "7.2.5" - name: Test compatibility run: npm test -- -t "set and get flow works" - working-directory: ./node + working-directory: ./node + + build-and-test-linux-musl-on-x86: + name: Build and test Node wrapper on Linux musl + runs-on: ubuntu-latest + container: + image: node:alpine + options: --user root --privileged + + steps: + - name: Install git + run: | + apk update + apk add git + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup musl on Linux + uses: ./.github/workflows/setup-musl-on-linux + with: + workspace: $GITHUB_WORKSPACE + npm-scope: ${{ secrets.NPM_SCOPE }} + npm-auth-token: ${{ secrets.NPM_AUTH_TOKEN }} + + - name: Build Node wrapper + uses: ./.github/workflows/build-node-wrapper + with: + os: ubuntu + named_os: linux + arch: x64 + target: x86_64-unknown-linux-musl + github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: "7.2.5" + + - name: Test compatibility + shell: bash + run: npm test -- -t "set and get flow works" + working-directory: ./node diff --git a/.github/workflows/npm-cd.yml b/.github/workflows/npm-cd.yml index 0c935cc64e..25db537ab5 100644 --- a/.github/workflows/npm-cd.yml +++ b/.github/workflows/npm-cd.yml @@ -1,6 +1,6 @@ # The cross platform build was created based on the [Packaging Rust Applications for the NPM Registry blog](https://blog.orhun.dev/packaging-rust-for-npm/). -name: Continuous Deployment +name: NPM - Continuous Deployment on: pull_request: @@ -8,98 +8,140 @@ on: - .github/workflows/npm-cd.yml - .github/workflows/build-node-wrapper/action.yml - .github/workflows/start-self-hosted-runner/action.yml + - .github/workflows/install-rust-and-protoc/action.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json push: tags: - "v*.*" + workflow_dispatch: + inputs: + version: + description: 'The release version of GLIDE, formatted as *.*.* or *.*.*-rc*' + required: true concurrency: group: npm-${{ github.head_ref || github.ref }} cancel-in-progress: true +permissions: + id-token: write + jobs: start-self-hosted-runner: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Start self hosted EC2 runner - uses: ./.github/workflows/start-self-hosted-runner - with: - aws-access-key-id: ${{ secrets.AWS_EC2_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_EC2_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - ec2-instance-id: ${{ secrets.AWS_EC2_INSTANCE_ID }} + if: github.repository_owner == 'valkey-io' + runs-on: ubuntu-latest + environment: AWS_ACTIONS + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Start self hosted EC2 runner + uses: ./.github/workflows/start-self-hosted-runner + with: + role-to-assume: ${{ secrets.ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + ec2-instance-id: ${{ secrets.AWS_EC2_INSTANCE_ID }} + + load-platform-matrix: + runs-on: ubuntu-latest + outputs: + PLATFORM_MATRIX: ${{ steps.load-platform-matrix.outputs.PLATFORM_MATRIX }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: load-platform-matrix + id: load-platform-matrix + shell: bash + run: | + # Get the matrix from the matrix.json file, without the object that has the IMAGE key + export "PLATFORM_MATRIX=$(jq 'map(select(.PACKAGE_MANAGERS | contains(["npm"])))' < .github/json_matrices/build-matrix.json | jq -c .)" + echo "PLATFORM_MATRIX=${PLATFORM_MATRIX}" >> $GITHUB_OUTPUT publish-binaries: - needs: start-self-hosted-runner - if: github.repository_owner == 'aws' + needs: [start-self-hosted-runner, load-platform-matrix] + if: github.repository_owner == 'valkey-io' name: Publish packages to NPM runs-on: ${{ matrix.build.RUNNER }} + container: + image: ${{ matrix.build.IMAGE || '' }} + options: ${{ matrix.build.CONTAINER_OPTIONS || 'none'}} strategy: fail-fast: false matrix: - build: - - { - OS: ubuntu-latest, - NAMED_OS: linux, - RUNNER: ubuntu-latest, - ARCH: x64, - TARGET: x86_64-unknown-linux-gnu, - } - - { - OS: ubuntu-latest, - NAMED_OS: linux, - RUNNER: [self-hosted, Linux, ARM64], - ARCH: arm64, - TARGET: aarch64-unknown-linux-gnu, - CONTAINER: "2_28", - } - - { - OS: macos-latest, - NAMED_OS: darwin, - RUNNER: macos-latest, - ARCH: x64, - TARGET: x86_64-apple-darwin, - } - - { - OS: macos-latest, - NAMED_OS: darwin, - RUNNER: macos-13-xlarge, - arch: arm64, - TARGET: aarch64-apple-darwin, - } + build: ${{fromJson(needs.load-platform-matrix.outputs.PLATFORM_MATRIX)}} steps: - name: Setup self-hosted runner access - if: ${{ contains(matrix.build.RUNNER, 'self-hosted') }} - run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/glide-for-redis + if: ${{ contains(matrix.build.RUNNER, 'self-hosted') && matrix.build.TARGET != 'aarch64-unknown-linux-musl' }} + run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide + # For MUSL on X64 we need to install git since we use the checkout action + - name: Install git for musl + if: ${{ contains(matrix.build.TARGET, 'x86_64-unknown-linux-musl')}} + run: | + apk update + apk add git + - name: Checkout + if: ${{ matrix.build.TARGET != 'aarch64-unknown-linux-musl' }} uses: actions/checkout@v4 with: submodules: "true" + fetch-depth: 0 + + - name: Setup for musl + if: ${{ contains(matrix.build.TARGET, 'musl')}} + uses: ./.github/workflows/setup-musl-on-linux + with: + workspace: $GITHUB_WORKSPACE + npm-scope: ${{ vars.NPM_SCOPE }} + npm-auth-token: ${{ secrets.NPM_AUTH_TOKEN }} + arch: ${{ matrix.build.ARCH }} - name: Set the release version shell: bash run: | - export version=`if ${{ github.event_name == 'pull_request' }}; then echo '255.255.255'; else echo ${GITHUB_REF:11}; fi` - echo "RELEASE_VERSION=${version}" >> $GITHUB_ENV + if ${{ env.EVENT_NAME == 'pull_request' }}; then + R_VERSION="255.255.255" + elif ${{ env.EVENT_NAME == 'workflow_dispatch' }}; then + R_VERSION="${{ env.INPUT_VERSION }}" + else + R_VERSION=${GITHUB_REF:11} + fi + echo "RELEASE_VERSION=${R_VERSION}" >> $GITHUB_ENV + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ github.event.inputs.version }} - name: Setup node + if: ${{ matrix.build.TARGET != 'aarch64-unknown-linux-musl' }} uses: actions/setup-node@v3 with: - node-version: "16" + node-version: "20" registry-url: "https://registry.npmjs.org" architecture: ${{ matrix.build.ARCH }} scope: "${{ vars.NPM_SCOPE }}" always-auth: true token: ${{ secrets.NPM_AUTH_TOKEN }} + - name: Setup node for publishing + if: ${{ matrix.build.TARGET == 'aarch64-unknown-linux-musl' }} + working-directory: ./node + run: | + npm config set registry https://registry.npmjs.org/ + npm config set '//registry.npmjs.org/:_authToken' ${{ secrets.NPM_AUTH_TOKEN }} + npm config set scope ${{ vars.NPM_SCOPE }} + - name: Update package version in config.toml uses: ./.github/workflows/update-glide-version with: folder_path: "${{ github.workspace }}/node/rust-client/.cargo" named_os: ${{ matrix.build.NAMED_OS }} - + - name: Build Node wrapper uses: ./.github/workflows/build-node-wrapper with: @@ -110,15 +152,39 @@ jobs: npm_scope: ${{ vars.NPM_SCOPE }} publish: "true" github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: "7.2.5" + + - name: Check if RC and set a distribution tag for the package + shell: bash + run: | + if [[ "${GITHUB_REF:11}" == *"rc"* ]] + then + echo "This is a release candidate: ${GITHUB_REF:11}" + export npm_tag="next" + else + echo "This is a stable release: ${GITHUB_REF:11}" + export npm_tag="latest" + fi + echo "NPM_TAG=${npm_tag}" >> $GITHUB_ENV + + - name: Check that the release version dont have typo init + if: ${{ github.event_name != 'pull_request' && contains(env.RELEASE_VERSION, '-') && !contains(env.RELEASE_VERSION, 'rc') }} + run: | + echo "The release version "${GITHUB_REF:11}" contains a typo, please fix it" + echo "The release version should be in the format v{major-version}.{minor-version}.{patch-version}-rc{release-candidate-number} when it a release candidate or v{major-version}.{minor-version}.{patch-version} in a stable release." + exit 1 - name: Publish to NPM if: github.event_name != 'pull_request' shell: bash working-directory: ./node run: | + npm pkg fix set +e - # Redirect only stderr - { npm_publish_err=$(npm publish --access public 2>&1 >&3 3>&-); } 3>&1 + # 2>&1 1>&3- redirects stderr to stdout and then redirects the original stdout to another file descriptor, + # effectively separating stderr and stdout. The 3>&1 at the end redirects the original stdout back to the console. + # https://github.com/npm/npm/issues/118#issuecomment-325440 - ignoring notice messages since currentlly they are directed to stderr + { npm_publish_err=$(npm publish --tag ${{ env.NPM_TAG }} --access public 2>&1 1>&3- | grep -v "notice") ;} 3>&1 if [[ "$npm_publish_err" == *"You cannot publish over the previously published versions"* ]] then echo "Skipping publishing, package already published" @@ -130,41 +196,30 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - - name: Pack the Node package + # Reset the repository to make sure we get the clean checkout of the action later in other actions. + # It is not required since in other actions we are cleaning before the action, but it is a good practice to do it here as well. + - name: Reset repository + if: ${{ matrix.build.ARCH == 'arm64' }} shell: bash - working-directory: ./node run: | - # Remove the "cpu" and "os" fileds so the base package would be able to install it on ubuntu - SED_FOR_MACOS=`if [[ "${{ matrix.build.OS }}" =~ .*"macos".* ]]; then echo "''"; fi` - sed -i $SED_FOR_MACOS '/"\/\/\/cpu": \[/,/]/d' ./package.json && sed -i $SED_FOR_MACOS '/"\/\/\/os": \[/,/]/d' ./package.json - mkdir -p bin - npm pack --pack-destination ./bin - ls ./bin - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - - - name: Upload the Node package - if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.build.TARGET }} - path: ./node/bin - if-no-files-found: error + git reset --hard + git clean -xdf publish-base-to-npm: + if: github.event_name != 'pull_request' name: Publish the base NPM package needs: publish-binaries runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: "true" - name: Install node uses: actions/setup-node@v3 with: - node-version: "16" + node-version: "20" registry-url: "https://registry.npmjs.org" scope: "${{ vars.NPM_SCOPE }}" always-auth: true @@ -173,7 +228,7 @@ jobs: shell: bash working-directory: ./node/npm/glide run: | - export pkg_name=glide-for-redis + export pkg_name=valkey-glide echo "${GITHUB_REF:11}" export package_version=${GITHUB_REF:11} export scope=`if [ "$NPM_SCOPE" != '' ]; then echo "$NPM_SCOPE/"; fi` @@ -188,36 +243,24 @@ jobs: - name: Build Node wrapper uses: ./.github/workflows/build-node-wrapper with: - os: ubuntu-latest + os: ubuntu target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Create a directory for the packed packages - shell: bash - working-directory: ./node/npm/glide - run: mkdir packages - - - name: Download the packed packages - id: download - uses: actions/download-artifact@v3 - with: - path: ./node/npm/glide/packages - - - name: Install the packed packages + engine-version: "7.2.5" + + - name: Check if RC and set a distribution tag for the package shell: bash - working-directory: ./node/npm/glide run: | - ls -LR packages/ - packages_list=`find ${{steps.download.outputs.download-path}} -type f -follow -print` - for package in $packages_list - do - if [[ "${package}" == *.tgz ]] - then - echo "Installing package $package" - npm i --no-save "$package" - fi - done - + if [[ "${GITHUB_REF:11}" == *"rc"* ]] + then + echo "This is a release candidate" + export npm_tag="next" + else + echo "This is a stable release" + export npm_tag="latest" + fi + echo "NPM_TAG=${npm_tag}" >> $GITHUB_ENV + - name: Publish the base package if: github.event_name != 'pull_request' shell: bash @@ -227,6 +270,105 @@ jobs: cp ../../README.md . npm install npm run build - npm publish --access public + npm publish --access public --tag ${{ env.NPM_TAG }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + + test-release: + if: github.event_name != 'pull_request' + name: Test the release + needs: [publish-base-to-npm, load-platform-matrix] + runs-on: ${{ matrix.build.RUNNER }} + container: + image: ${{ matrix.build.IMAGE || '' }} + options: ${{ matrix.build.CONTAINER_OPTIONS || 'none'}} + strategy: + fail-fast: false + matrix: + build: ${{fromJson(needs.load-platform-matrix.outputs.PLATFORM_MATRIX)}} + steps: + - name: Setup self-hosted runner access + if: ${{ matrix.build.TARGET == 'aarch64-unknown-linux-gnu' }} + run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide + + - name: install Redis and git for alpine + if: ${{ contains(matrix.build.TARGET, 'musl') }} + run: | + apk update + apk add redis git + node -v + + - name: install Redis and Python for ubuntu + if: ${{ contains(matrix.build.TARGET, 'linux-gnu') }} + run: | + sudo apt-get update + sudo apt-get install redis-server python3 + + - name: install Redis, Python for macos + if: ${{ contains(matrix.build.RUNNER, 'mac') }} + run: | + brew install redis python3 + + - name: Checkout + if: ${{ matrix.build.TARGET != 'aarch64-unknown-linux-musl'}} + uses: actions/checkout@v4 + with: + submodules: "true" + + - name: Setup for musl + if: ${{ contains(matrix.build.TARGET, 'musl') }} + uses: ./.github/workflows/setup-musl-on-linux + with: + workspace: $GITHUB_WORKSPACE + npm-scope: ${{ vars.NPM_SCOPE }} + npm-auth-token: ${{ secrets.NPM_AUTH_TOKEN }} + arch: ${{ matrix.build.ARCH }} + + - name: Setup node + if: ${{ matrix.build.TARGET != 'aarch64-unknown-linux-musl' }} + uses: actions/setup-node@v3 + with: + node-version: "16" + registry-url: "https://registry.npmjs.org" + architecture: ${{ matrix.build.ARCH }} + scope: "${{ vars.NPM_SCOPE }}" + always-auth: true + token: ${{ secrets.NPM_AUTH_TOKEN }} + + - name: Install tsc and compile utils + shell: bash + working-directory: ./utils + run: | + npm install + npm install -g typescript + npx tsc -p ./tsconfig.json + + - name: Check if RC and set a distribution tag for the package + shell: bash + run: | + if [[ "${GITHUB_REF:11}" == *"rc"* ]] + then + echo "This is a release candidate" + export npm_tag="next" + else + echo "This is a stable release" + export npm_tag="latest" + fi + echo "NPM_TAG=${npm_tag}" >> $GITHUB_ENV + + - name: Run the tests + shell: bash + working-directory: ./utils/release-candidate-testing/node + run: | + npm install + npm install --no-save @valkey/valkey-glide@${{ env.NPM_TAG }} + npm run test + + # Reset the repository to make sure we get the clean checkout of the action later in other actions. + # It is not required since in other actions we are cleaning before the action, but it is a good practice to do it here as well. + - name: Reset repository + if: ${{ contains(matrix.build.RUNNER, 'self-hosted') }} + shell: bash + run: | + git reset --hard + git clean -xdf diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index b6c0508eb8..fcea61ee6b 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -19,7 +19,7 @@ on: required: true jobs: run-ort: - if: github.repository_owner == 'aws' + if: github.repository_owner == 'valkey-io' name: Create attribution files runs-on: ubuntu-latest strategy: @@ -28,6 +28,7 @@ jobs: PYTHON_ATTRIBUTIONS: "python/THIRD_PARTY_LICENSES_PYTHON" NODE_ATTRIBUTIONS: "node/THIRD_PARTY_LICENSES_NODE" RUST_ATTRIBUTIONS: "glide-core/THIRD_PARTY_LICENSES_RUST" + JAVA_ATTRIBUTIONS: "java/THIRD_PARTY_LICENSES_JAVA" steps: - name: Set the release version shell: bash @@ -52,11 +53,6 @@ jobs: with: submodules: "true" ref: ${{ env.BASE_BRANCH }} - # This is a temporary fix, till ORT will fix thire issue with newer v of Cargo - https://github.com/oss-review-toolkit/ort/issues/8480 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.76 - with: - targets: ${{ inputs.target }} - name: Set up JDK 11 for the ORT package uses: actions/setup-java@v4 @@ -80,19 +76,11 @@ jobs: with: repository: "oss-review-toolkit/ort" path: "./ort" - ref: main + ref: "26.0.0" submodules: recursive - - name: Checkout ORT latest release tag - if: steps.cache-ort.outputs.cache-hit != 'true' - working-directory: ./ort/ - run: | - # Get new tags from remote - git fetch --tags - # Get latest tag name - LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)") - # Checkout latest tag - git checkout $LATEST_TAG + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.78 - name: Install ORT if: steps.cache-ort.outputs.cache-hit != 'true' @@ -108,7 +96,7 @@ jobs: ort: analyzer: allowDynamicVersions: true - enabledPackageManagers: [Cargo, NPM, PIP] + enabledPackageManagers: [Cargo, NPM, PIP, GradleInspector] EOF cat ~/.ort/config/config.yml @@ -130,7 +118,7 @@ jobs: run: | # Remove the glide-rs dependency to avoid duplication sed -i '/ "glide-rs":/d' ../../package.json - export pkg_name=glide-for-redis-base + export pkg_name=valkey-glide-base export package_version="${{ env.RELEASE_VERSION }}" export scope=`if [ "$NPM_SCOPE" != '' ]; then echo "$NPM_SCOPE/"; fi` mv package.json package.json.tmpl @@ -167,6 +155,19 @@ jobs: with: folder_path: "${{ github.workspace }}/glide-core" + ### Java ### + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: 11 + + - name: Run ORT tools for Java + uses: ./.github/workflows/run-ort-tools + with: + folder_path: "${{ github.workspace }}/java" + ### Process results ### - name: Check for diff @@ -174,7 +175,8 @@ jobs: cp python/ort_results/NOTICE_DEFAULT $PYTHON_ATTRIBUTIONS cp node/ort_results/NOTICE_DEFAULT $NODE_ATTRIBUTIONS cp glide-core/ort_results/NOTICE_DEFAULT $RUST_ATTRIBUTIONS - GIT_DIFF=`git diff $PYTHON_ATTRIBUTIONS $NODE_ATTRIBUTIONS $RUST_ATTRIBUTIONS` + cp java/ort_results/NOTICE_DEFAULT $JAVA_ATTRIBUTIONS + GIT_DIFF=`git diff $PYTHON_ATTRIBUTIONS $NODE_ATTRIBUTIONS $RUST_ATTRIBUTIONS $JAVA_ATTRIBUTIONS` if [ -n "$GIT_DIFF" ]; then echo "FOUND_DIFF=true" >> $GITHUB_ENV else @@ -197,11 +199,11 @@ jobs: run: | export BRANCH_NAME=`if [ "$EVENT_NAME" == 'schedule' ] || [ "$EVENT_NAME" == 'pull_request' ]; then echo 'scheduled-ort'; else echo "ort-v$INPUT_VERSION"; fi` echo "Creating pull request from branch ${BRANCH_NAME} to branch ${{ env.BASE_BRANCH }}" - git config --global user.email "glide-for-redis@amazon.com" + git config --global user.email "valkey-glide@lists.valkey.io" git config --global user.name "ort-bot" git checkout -b ${BRANCH_NAME} - git add $PYTHON_ATTRIBUTIONS $NODE_ATTRIBUTIONS $RUST_ATTRIBUTIONS - git commit -m "Updated attribution files" + git add $PYTHON_ATTRIBUTIONS $NODE_ATTRIBUTIONS $RUST_ATTRIBUTIONS $JAVA_ATTRIBUTIONS + git commit -m "Updated attribution files" -s git push --set-upstream origin ${BRANCH_NAME} -f title="Updated attribution files for ${BRANCH_NAME}" gh pr create -B ${{ env.BASE_BRANCH }} -H ${BRANCH_NAME} --title "${title}" --body 'Created by Github action.\n${{ env.LICENSES_LIST }}' @@ -209,4 +211,27 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} EVENT_NAME: ${{ github.event_name }} INPUT_VERSION: ${{ github.event.inputs.version }} - + + - name: Get current date + id: date + run: | + CURR_DATE=$(date +'%Y-%m-%d-%H') + echo "date=${CURR_DATE}" >> $GITHUB_OUTPUT + + - name: Upload the final package list + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: final-package-list-${{ steps.date.outputs.date }} + path: | + utils/final_package_list.txt + retention-days: 30 + + - name: Upload the skipped package list + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: skipped-package-list-${{ steps.date.outputs.date }} + path: | + utils/skipped_package_list.txt + retention-days: 30 diff --git a/.github/workflows/pypi-cd.yml b/.github/workflows/pypi-cd.yml index 93fe10f9f1..b45992e6e7 100644 --- a/.github/workflows/pypi-cd.yml +++ b/.github/workflows/pypi-cd.yml @@ -1,6 +1,6 @@ # The cross platform build was created based on the [Packaging Rust Applications for the NPM Registry blog](https://blog.orhun.dev/packaging-rust-for-npm/). -name: Continuous Deployment +name: PyPI - Continuous Deployment on: pull_request: @@ -8,31 +8,60 @@ on: - .github/workflows/pypi-cd.yml - .github/workflows/build-python-wrapper/action.yml - .github/workflows/start-self-hosted-runner/action.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json push: tags: - "v*.*" + workflow_dispatch: + inputs: + version: + description: 'The release version of GLIDE, formatted as *.*.* or *.*.*-rc*' + required: true concurrency: group: pypi-${{ github.head_ref || github.ref }} cancel-in-progress: true +permissions: + id-token: write + jobs: + load-platform-matrix: + runs-on: ubuntu-latest + outputs: + PLATFORM_MATRIX: ${{ steps.load-platform-matrix.outputs.PLATFORM_MATRIX }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: load-platform-matrix + id: load-platform-matrix + shell: bash + run: | + # Get the matrix from the matrix.json file, without the object that has the IMAGE key + export "PLATFORM_MATRIX=$(jq 'map(select(.PACKAGE_MANAGERS | contains(["pypi"])))' < .github/json_matrices/build-matrix.json | jq -c .)" + echo "PLATFORM_MATRIX=${PLATFORM_MATRIX}" >> $GITHUB_OUTPUT + + start-self-hosted-runner: + if: github.repository_owner == 'valkey-io' runs-on: ubuntu-latest + environment: AWS_ACTIONS steps: - name: Checkout uses: actions/checkout@v4 - name: Start self hosted EC2 runner uses: ./.github/workflows/start-self-hosted-runner with: - aws-access-key-id: ${{ secrets.AWS_EC2_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_EC2_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.ROLE_TO_ASSUME }} aws-region: ${{ secrets.AWS_REGION }} ec2-instance-id: ${{ secrets.AWS_EC2_INSTANCE_ID }} publish-binaries: - needs: start-self-hosted-runner - if: github.repository_owner == 'aws' + needs: [start-self-hosted-runner, load-platform-matrix] + if: github.repository_owner == 'valkey-io' name: Publish packages to PyPi runs-on: ${{ matrix.build.RUNNER }} timeout-minutes: 25 @@ -40,39 +69,11 @@ jobs: fail-fast: false matrix: build: - - { - OS: ubuntu-latest, - NAMED_OS: linux, - RUNNER: ubuntu-latest, - ARCH: x64, - TARGET: x86_64-unknown-linux-gnu, - } - - { - OS: ubuntu-latest, - NAMED_OS: linux, - RUNNER: [self-hosted, Linux, ARM64], - ARCH: arm64, - TARGET: aarch64-unknown-linux-gnu, - CONTAINER: "2_28", - } - - { - OS: macos-latest, - NAMED_OS: darwin, - RUNNER: macos-latest, - ARCH: x64, - TARGET: x86_64-apple-darwin, - } - - { - OS: macos-latest, - NAMED_OS: darwin, - RUNNER: macos-13-xlarge, - arch: arm64, - TARGET: aarch64-apple-darwin, - } + ${{fromJson( needs.load-platform-matrix.outputs.PLATFORM_MATRIX )}} steps: - name: Setup self-hosted runner access if: ${{ contains(matrix.build.RUNNER, 'self-hosted') }} - run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/glide-for-redis + run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide - name: Checkout uses: actions/checkout@v4 @@ -82,8 +83,24 @@ jobs: - name: Set the release version shell: bash run: | - export version=`if ${{ github.event_name == 'pull_request' }}; then echo '255.255.255'; else echo ${GITHUB_REF:11}; fi` - echo "RELEASE_VERSION=${version}" >> $GITHUB_ENV + if ${{ env.EVENT_NAME == 'pull_request' }}; then + R_VERSION="255.255.255" + elif ${{ env.EVENT_NAME == 'workflow_dispatch' }}; then + R_VERSION="${{ env.INPUT_VERSION }}" + else + R_VERSION=${GITHUB_REF:11} + fi + echo "RELEASE_VERSION=${R_VERSION}" >> $GITHUB_ENV + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ github.event.inputs.version }} + + - name: Check that the release version dont have typo init + if: ${{ github.event_name != 'pull_request' && contains(env.RELEASE_VERSION, '-') && !contains(env.RELEASE_VERSION, 'rc') }} + run: | + echo "The release version "${{ env.RELEASE_VERSION }}" contains a typo, please fix it" + echo "The release version should be in the format v{major-version}.{minor-version}.{patch-version}-rc{release-candidate-number} when it a release candidate or v{major-version}.{minor-version}.{patch-version} in a stable release." + exit 1 - name: Set the package version for Python working-directory: ./python @@ -126,6 +143,7 @@ jobs: target: ${{ matrix.build.TARGET }} publish: "true" github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: "7.2.5" - name: Include protobuf files in the package working-directory: ./python @@ -141,20 +159,29 @@ jobs: with: working-directory: ./python target: ${{ matrix.build.TARGET }} - args: --release --strip --out wheels -i ${{ github.event_name != 'pull_request' && 'python3.8 python3.9 python3.10 python3.11 python3.12' || 'python3.10' }} + args: --release --strip --out wheels -i ${{ github.event_name != 'pull_request' && 'python3.8 python3.9 python3.10 python3.11 python3.12' || 'python3.10' }} manylinux: auto container: ${{ matrix.build.CONTAINER != '' && matrix.build.CONTAINER || '2014' }} before-script-linux: | # Install protobuf compiler if [[ $(`which apt`) != '' ]] - then - apt install protobuf-compiler -y - else - PB_REL="https://github.com/protocolbuffers/protobuf/releases" - curl -LO $PB_REL/download/v3.15.8/protoc-3.15.8-linux-x86_64.zip - unzip protoc-3.15.8-linux-x86_64.zip -d $HOME/.local - export PATH="$PATH:$HOME/.local/bin" + then + echo "installing unzip and curl" + apt install unzip curl -y + fi + PB_REL="https://github.com/protocolbuffers/protobuf/releases" + ARCH=`uname -p` + if [[ $ARCH == 'x86_64' ]]; then + PROTOC_ARCH="x86_64" + elif [[ $ARCH == 'aarch64' ]]; then + PROTOC_ARCH="aarch_64" + else + echo "Running on unsupported architecture: $ARCH. Expected one of: ['x86_64', 'aarch64']" + exit 1 fi + curl -LO $PB_REL/download/v3.20.3/protoc-3.20.3-linux-${PROTOC_ARCH}.zip + unzip protoc-3.20.3-linux-${PROTOC_ARCH}.zip -d $HOME/.local + export PATH="$PATH:$HOME/.local/bin" - name: Build Python wheels (macos) if: startsWith(matrix.build.NAMED_OS, 'darwin') diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 59844f06f8..ee3ee6cc95 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -11,7 +11,10 @@ on: - .github/workflows/python.yml - .github/workflows/build-python-wrapper/action.yml - .github/workflows/install-shared-dependencies/action.yml - - .github/workflows/install-redis-modules/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json pull_request: paths: @@ -22,7 +25,11 @@ on: - .github/workflows/python.yml - .github/workflows/build-python-wrapper/action.yml - .github/workflows/install-shared-dependencies/action.yml - - .github/workflows/install-redis-modules/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: python-${{ github.head_ref || github.ref }} @@ -32,30 +39,55 @@ permissions: contents: read jobs: - test-ubuntu-latest: - runs-on: ubuntu-latest - timeout-minutes: 25 + load-engine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + + test: + runs-on: ${{ matrix.host.RUNNER }} + needs: load-engine-matrix + timeout-minutes: 35 strategy: fail-fast: false matrix: - redis: - - 6.2.14 - - 7.2.3 + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + python: + # - "3.8" + # - "3.9" + # - "3.10" + # - "3.11" + - "3.12" + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + ## TODO comment out macos + - { + OS: macos, + RUNNER: macos-latest, + TARGET: aarch64-apple-darwin + } steps: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install redis - uses: ./.github/workflows/install-redis - with: - redis-version: ${{ matrix.redis }} - - - name: Set up Python 3.10 + - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: ${{ matrix.python }} - name: Install dependencies working-directory: ./python @@ -66,9 +98,10 @@ jobs: - name: Build Python wrapper uses: ./.github/workflows/build-python-wrapper with: - os: "ubuntu-latest" - target: "x86_64-unknown-linux-gnu" + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: ${{ matrix.engine.version }} - name: Type check with mypy working-directory: ./python @@ -77,21 +110,16 @@ jobs: # all installed dependencies and build files source .env/bin/activate pip install mypy types-protobuf - # Install the benchmark requirements + # Install the benchmark requirements pip install -r ../benchmarks/python/requirements.txt python -m mypy .. - - name: Install Redis Modules - uses: ./.github/workflows/install-redis-modules - with: - redis-version: ${{ matrix.redis }} - - name: Test with pytest working-directory: ./python run: | source .env/bin/activate cd python/tests/ - pytest --asyncio-mode=auto --override-ini=addopts= --load-module=$GITHUB_WORKSPACE/redisjson.so --html=pytest_report.html --self-contained-html + pytest --asyncio-mode=auto --html=pytest_report.html --self-contained-html - uses: ./.github/workflows/test-benchmark with: @@ -102,12 +130,73 @@ jobs: continue-on-error: true uses: actions/upload-artifact@v4 with: - name: test-reports-python-redis-${{ matrix.redis }} + name: test-report-python-${{ matrix.python }}-${{ matrix.engine.type }}-${{ matrix.engine.version }}-${{ matrix.host.RUNNER }} path: | python/python/tests/pytest_report.html utils/clusters/** benchmarks/results/** + test-pubsub: + runs-on: ${{ matrix.host.RUNNER }} + needs: load-engine-matrix + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} + python: + # - "3.8" + # - "3.9" + # - "3.10" + # - "3.11" + - "3.12" + host: + - { + OS: ubuntu, + RUNNER: ubuntu-latest, + TARGET: x86_64-unknown-linux-gnu + } + ## TODO comment out macos + - { + OS: macos, + RUNNER: macos-latest, + TARGET: aarch64-apple-darwin + } + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Build Python wrapper + uses: ./.github/workflows/build-python-wrapper + with: + os: ${{ matrix.host.OS }} + target: ${{ matrix.host.TARGET }} + github-token: ${{ secrets.GITHUB_TOKEN }} + engine-version: ${{ matrix.engine.version }} + + - name: Test pubsub with pytest + working-directory: ./python + run: | + source .env/bin/activate + cd python/tests/ + pytest --asyncio-mode=auto -k test_pubsub --html=pytest_report.html --self-contained-html + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: pubsub-test-report-python-${{ matrix.python }}-${{ matrix.engine.type }}-${{ matrix.engine.version }}-${{ matrix.host.RUNNER }} + path: | + python/python/tests/pytest_report.html + lint: runs-on: ubuntu-latest timeout-minutes: 15 @@ -149,29 +238,6 @@ jobs: run: | black --check --diff . - build-macos-latest: - runs-on: macos-latest - timeout-minutes: 25 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Set up Homebrew - uses: Homebrew/actions/setup-homebrew@master - - - name: Build Python wrapper - uses: ./.github/workflows/build-python-wrapper - with: - os: "macos-latest" - target: "x86_64-apple-darwin" - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Test compatibility with pytest - working-directory: ./python - run: | - source .env/bin/activate - pytest --asyncio-mode=auto -m smoke_test - build-amazonlinux-latest: runs-on: ubuntu-latest container: amazonlinux:latest @@ -182,7 +248,7 @@ jobs: yum -y remove git yum -y remove git-* yum -y install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm - yum install -y git + yum install -y git git --version - uses: actions/checkout@v4 @@ -202,15 +268,19 @@ jobs: os: "amazon-linux" target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Create a symbolic Link for redis6 binaries - working-directory: ./python - run: | - ln -s /usr/bin/redis6-server /usr/bin/redis-server - ln -s /usr/bin/redis6-cli /usr/bin/redis-cli + engine-version: "7.2.5" - name: Test compatibility with pytest working-directory: ./python run: | source .env/bin/activate - pytest --asyncio-mode=auto -m smoke_test + pytest --asyncio-mode=auto -m smoke_test --html=pytest_report.html --self-contained-html + + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: smoke-test-report-amazon-linux + path: | + python/python/tests/pytest_report.html diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 87b1a17dc1..2211a04f0f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,6 +9,9 @@ on: - submodules/** - utils/cluster_manager.py - .github/workflows/rust.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json pull_request: paths: - logger_core/** @@ -16,6 +19,10 @@ on: - submodules/** - utils/cluster_manager.py - .github/workflows/rust.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-valkey/action.yml + - .github/json_matrices/build-matrix.json + workflow_dispatch: concurrency: group: rust-${{ github.head_ref || github.ref }} @@ -25,33 +32,40 @@ env: CARGO_TERM_COLOR: always jobs: + load-engine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.load-engine-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load the engine matrix + id: load-engine-matrix + shell: bash + run: echo "matrix=$(jq -c . < .github/json_matrices/engine-matrix.json)" >> $GITHUB_OUTPUT + build: runs-on: ubuntu-latest + needs: load-engine-matrix timeout-minutes: 15 strategy: fail-fast: false matrix: - redis: - - 6.2.14 - - 7.2.3 + engine: ${{ fromJson(needs.load-engine-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install redis - uses: ./.github/workflows/install-redis - with: - redis-version: ${{ matrix.redis }} - - name: Install shared software dependencies uses: ./.github/workflows/install-shared-dependencies with: - os: "ubuntu-latest" + os: "ubuntu" target: "x86_64-unknown-linux-gnu" + engine-version: ${{ matrix.engine.version }} - - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run tests @@ -83,14 +97,17 @@ jobs: - uses: ./.github/workflows/lint-rust with: cargo-toml-folder: ./glide-core + github-token: ${{ secrets.GITHUB_TOKEN }} name: lint glide-core - uses: ./.github/workflows/lint-rust with: cargo-toml-folder: ./logger_core + github-token: ${{ secrets.GITHUB_TOKEN }} name: lint logger - uses: ./.github/workflows/lint-rust with: cargo-toml-folder: ./benchmarks/rust + github-token: ${{ secrets.GITHUB_TOKEN }} name: lint benchmark diff --git a/.github/workflows/setup-musl-on-linux/action.yml b/.github/workflows/setup-musl-on-linux/action.yml new file mode 100644 index 0000000000..ed4677b74a --- /dev/null +++ b/.github/workflows/setup-musl-on-linux/action.yml @@ -0,0 +1,61 @@ +name: Setup musl on Linux + +inputs: + workspace: + description: "github workspace" + required: true + type: string + npm-scope: + description: "npm scope" + required: true + type: string + npm-auth-token: + description: "npm auth token" + required: true + type: string + arch: + description: "architecture" + required: false + type: string + options: + - "arm64" + - "x64" + default: "arm64" + +runs: + using: "composite" + steps: + - name: Install dependencies + shell: sh + run: | + apk update + apk add bash git sed python3 + + - name: Skip all steps if not on ARM64 + shell: bash + if: ${{ inputs.arch != 'arm64' }} + run: exit 0 + + # Currently "Checkout" action is not supported for musl on ARM64, so the checkout is happening on the runner and + # here we just making sure we getting the clean repo + - name: Clean repository for musl on ARM64 + shell: bash + run: | + git config --global --add safe.directory "${{ inputs.workspace }}" + git clean -xdf + git reset --hard + git submodule sync + git submodule update --init --recursive + + - name: Set up access for musl on ARM + shell: bash + run: | + chown -R $(whoami):$(whoami) $GITHUB_WORKSPACE + + - name: Setup node + shell: bash + working-directory: ./node + run: | + npm config set registry https://registry.npmjs.org/ + npm config set '//registry.npmjs.org/:_authToken' ${{ inputs.npm-auth-token }} + npm config set scope ${{ inputs.npm-scope }} diff --git a/.github/workflows/start-self-hosted-runner/action.yml b/.github/workflows/start-self-hosted-runner/action.yml index 6433ec69aa..2e99795c67 100644 --- a/.github/workflows/start-self-hosted-runner/action.yml +++ b/.github/workflows/start-self-hosted-runner/action.yml @@ -4,11 +4,8 @@ inputs: aws-region: description: AWS Region, e.g. us-east-1 required: true - aws-access-key-id: - description: AWS Access Key ID. Provide this key if you want to assume a role using access keys rather than a web identity token. - required: true - aws-secret-access-key: - description: AWS Secret Access Key. Required if aws-access-key-id is provided. + role-to-assume: + description: AWS Role for which to fetch credentials. required: true ec2-instance-id: description: AWS EC2 instance ID for the self hosted runner @@ -20,13 +17,28 @@ runs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ inputs.aws-access-key-id }} - aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + role-to-assume: ${{ inputs.role-to-assume }} aws-region: ${{ inputs.aws-region }} + - name: Start EC2 self hosted runner shell: bash run: | sudo apt update sudo apt install awscli -y - aws ssm send-command --instance-ids ${{ inputs.ec2-instance-id }} --document-name StartGithubSelfHostedRunner --output text - aws ssm list-command-invocations + command_id=$(aws ssm send-command --instance-ids ${{ inputs.ec2-instance-id }} --document-name StartGithubSelfHostedRunner --query Command.CommandId --output text) + + while [[ "$invoke_status" != "Success" && "$invoke_status" != "Failed" ]]; do + invoke_status=$(aws ssm list-command-invocations --command-id $command_id --query 'CommandInvocations[0].Status' --output text) + echo "Current Status: $invoke_status" + if [[ "$invoke_status" != "Success" && "$invoke_status" != "Failed" ]]; then + echo "Waiting for command to complete... Command ID: $command_id" + sleep 10 + retry_counter=$((retry_counter+1)) + fi + if (( retry_counter >= 100 )); then + echo "Reached maximum retries, status is: $invoke_status. Exiting..." + exit 1 + fi + done + + echo "Final Command Status: $invoke_status" diff --git a/.gitignore b/.gitignore index fdc9d2b163..573bfc218d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ glide-logs/ logger-rs.linux-x64-gnu.node utils/clusters/ utils/tls_crts/ +utils/TestUtils.js # OSS Review Toolkit (ORT) files **/ort*/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 041e0c877c..a4e304e30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +1,245 @@ #### Changes -* Python: Added JSON.DEL JSON.FORGET commands ([#1146](https://github.com/aws/glide-for-redis/pull/1146)) -* Python: Added STRLEN command ([#1230](https://github.com/aws/glide-for-redis/pull/1230)) -* Python: Added HKEYS command ([#1228](https://github.com/aws/glide-for-redis/pull/1228)) -* Python: Added RPUSHX and LPUSHX commands ([#1239](https://github.com/aws/glide-for-redis/pull/1239)) -* Python: Added ZREMRANGEBYSCORE command ([#1151](https://github.com/aws/glide-for-redis/pull/1151)) -* Node, Python: Added SPOP, SPOPCOUNT commands. ([#1117](https://github.com/aws/glide-for-redis/pull/1117), [#1261](https://github.com/aws/glide-for-redis/pull/1261)) -* Node: Added ZRANGE command ([#1115](https://github.com/aws/glide-for-redis/pull/1115)) -* Python: Added RENAME command ([#1252](https://github.com/aws/glide-for-redis/pull/1252)) -* Python: Added APPEND command ([#1152](https://github.com/aws/glide-for-redis/pull/1152)) -* Python: Added GEOADD command ([#1259](https://github.com/aws/glide-for-redis/pull/1259)) -* Python: Added GEOHASH command ([#1281](https://github.com/aws/glide-for-redis/pull/1281)) -* Python: Added ZLEXCOUNT command ([#1305](https://github.com/aws/glide-for-redis/pull/1305)) -* Python: Added ZREMRANGEBYLEX command ([#1306](https://github.com/aws/glide-for-redis/pull/1306)) -* Python: Added LINSERT command ([#1304](https://github.com/aws/glide-for-redis/pull/1304)) +* Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) +* Node: Added LSET command ([#1952](https://github.com/valkey-io/valkey-glide/pull/1952)) +* Node: Added SDIFFSTORE command ([#1931](https://github.com/valkey-io/valkey-glide/pull/1931)) +* Node: Added SINTERCARD command ([#1956](https://github.com/valkey-io/valkey-glide/pull/1956)) +* Node: Added SINTERSTORE command ([#1929](https://github.com/valkey-io/valkey-glide/pull/1929)) +* Node: Added SUNION command ([#1919](https://github.com/valkey-io/valkey-glide/pull/1919)) +* Node: Added SMISMEMBER command ([#1955](https://github.com/valkey-io/valkey-glide/pull/1955)) +* Node: Added SDIFF command ([#1924](https://github.com/valkey-io/valkey-glide/pull/1924)) +* Node: Added LOLWUT command ([#1934](https://github.com/valkey-io/valkey-glide/pull/1934)) +* Node: Added LPOS command ([#1927](https://github.com/valkey-io/valkey-glide/pull/1927)) + +## 1.0.0 (2024-07-09) + +#### Changes +* Node: Added ZINTERSTORE command ([#1513](https://github.com/valkey-io/valkey-glide/pull/1513)) +* Python: Added OBJECT ENCODING command ([#1471](https://github.com/valkey-io/valkey-glide/pull/1471)) +* Python: Added OBJECT FREQ command ([#1472](https://github.com/valkey-io/valkey-glide/pull/1472)) +* Python: Added OBJECT IDLETIME command ([#1474](https://github.com/valkey-io/valkey-glide/pull/1474)) +* Python: Added GEOSEARCH command ([#1482](https://github.com/valkey-io/valkey-glide/pull/1482)) +* Python: Added GEOSEARCHSTORE command ([#1581](https://github.com/valkey-io/valkey-glide/pull/1581)) +* Node: Added RENAMENX command ([#1483](https://github.com/valkey-io/valkey-glide/pull/1483)) +* Python: Added OBJECT REFCOUNT command ([#1485](https://github.com/valkey-io/valkey-glide/pull/1485)) +* Python: Added RENAMENX command ([#1492](https://github.com/valkey-io/valkey-glide/pull/1492)) +* Python: Added PFCOUNT command ([#1493](https://github.com/valkey-io/valkey-glide/pull/1493)) +* Python: Added PFMERGE command ([#1497](https://github.com/valkey-io/valkey-glide/pull/1497)) +* Node: Added SINTER command ([#1500](https://github.com/valkey-io/valkey-glide/pull/1500)) +* Python: Added XLEN command ([#1503](https://github.com/valkey-io/valkey-glide/pull/1503)) +* Python: Added LASTSAVE command ([#1509](https://github.com/valkey-io/valkey-glide/pull/1509)) +* Python: Added GETDEL command ([#1514](https://github.com/valkey-io/valkey-glide/pull/1514)) +* Python: Added GETRANGE command ([#1585](https://github.com/valkey-io/valkey-glide/pull/1585)) +* Python: Added ZINTER, ZUNION commands ([#1478](https://github.com/valkey-io/valkey-glide/pull/1478)) +* Python: Added SINTERCARD command ([#1511](https://github.com/valkey-io/valkey-glide/pull/1511)) +* Python: Added SORT command ([#1439](https://github.com/valkey-io/valkey-glide/pull/1439)) +* Node: Added OBJECT ENCODING command ([#1518](https://github.com/valkey-io/valkey-glide/pull/1518), [#1559](https://github.com/valkey-io/valkey-glide/pull/1559)) +* Python: Added LMOVE and BLMOVE commands ([#1536](https://github.com/valkey-io/valkey-glide/pull/1536)) +* Node: Added SUNIONSTORE command ([#1549](https://github.com/valkey-io/valkey-glide/pull/1549)) +* Python: Added SUNION command ([#1583](https://github.com/valkey-io/valkey-glide/pull/1583)) +* Node: Added PFCOUNT command ([#1545](https://github.com/valkey-io/valkey-glide/pull/1545)) +* Node: Added OBJECT FREQ command ([#1542](https://github.com/valkey-io/valkey-glide/pull/1542), [#1559](https://github.com/valkey-io/valkey-glide/pull/1559)) +* Node: Added LINSERT command ([#1544](https://github.com/valkey-io/valkey-glide/pull/1544)) +* Node: Added XLEN command ([#1555](https://github.com/valkey-io/valkey-glide/pull/1555)) +* Node: Added ZINTERCARD command ([#1553](https://github.com/valkey-io/valkey-glide/pull/1553)) +* Python: Added ZINCBY command ([#1586](https://github.com/valkey-io/valkey-glide/pull/1586)) +* Python: Added LMPOP and BLMPOP commands ([#1547](https://github.com/valkey-io/valkey-glide/pull/1547)) +* Python: Added HSTRLEN command ([#1564](https://github.com/valkey-io/valkey-glide/pull/1564)) +* Python: Added MSETNX command ([#1565](https://github.com/valkey-io/valkey-glide/pull/1565)) +* Python: Added MOVE command ([#1566](https://github.com/valkey-io/valkey-glide/pull/1566)) +* Python: Added EXPIRETIME, PEXPIRETIME commands ([#1587](https://github.com/valkey-io/valkey-glide/pull/1587)) +* Python: Added LSET command ([#1584](https://github.com/valkey-io/valkey-glide/pull/1584)) +* Node: Added OBJECT IDLETIME command ([#1567](https://github.com/valkey-io/valkey-glide/pull/1567)) +* Node: Added OBJECT REFCOUNT command ([#1568](https://github.com/valkey-io/valkey-glide/pull/1568)) +* Python: Added SETBIT command ([#1571](https://github.com/valkey-io/valkey-glide/pull/1571)) +* Python: Added SRandMember command ([#1578](https://github.com/valkey-io/valkey-glide/pull/1578)) +* Python: Added GETBIT command ([#1575](https://github.com/valkey-io/valkey-glide/pull/1575)) +* Python: Added BITCOUNT command ([#1592](https://github.com/valkey-io/valkey-glide/pull/1592)) +* Python: Added FLUSHALL command ([#1579](https://github.com/valkey-io/valkey-glide/pull/1579)) +* Python: Added TOUCH command ([#1582](https://github.com/valkey-io/valkey-glide/pull/1582)) +* Python: Added BITOP command ([#1596](https://github.com/valkey-io/valkey-glide/pull/1596)) +* Python: Added BITPOS command ([#1604](https://github.com/valkey-io/valkey-glide/pull/1604)) +* Python: Added GETEX command ([#1612](https://github.com/valkey-io/valkey-glide/pull/1612)) +* Python: Added BITFIELD and BITFIELD_RO commands ([#1615](https://github.com/valkey-io/valkey-glide/pull/1615)) +* Python: Added ZREVRANK command ([#1614](https://github.com/valkey-io/valkey-glide/pull/1614)) +* Python: Added XDEL command ([#1619](https://github.com/valkey-io/valkey-glide/pull/1619)) +* Python: Added XRANGE command ([#1624](https://github.com/valkey-io/valkey-glide/pull/1624)) +* Python: Added COPY command ([#1626](https://github.com/valkey-io/valkey-glide/pull/1626)) +* Python: Added XREVRANGE command ([#1625](https://github.com/valkey-io/valkey-glide/pull/1625)) +* Python: Added XREAD command ([#1644](https://github.com/valkey-io/valkey-glide/pull/1644)) +* Python: Added XGROUP CREATE and XGROUP DESTROY commands ([#1646](https://github.com/valkey-io/valkey-glide/pull/1646)) +* Python: Added XGROUP CREATECONSUMER and XGROUP DELCONSUMER commands ([#1658](https://github.com/valkey-io/valkey-glide/pull/1658)) +* Python: Added LOLWUT command ([#1657](https://github.com/valkey-io/valkey-glide/pull/1657)) +* Python: Added XREADGROUP command ([#1679](https://github.com/valkey-io/valkey-glide/pull/1679)) +* Python: Added XACK command ([#1681](https://github.com/valkey-io/valkey-glide/pull/1681)) +* Python: Added FLUSHDB command ([#1680](https://github.com/valkey-io/valkey-glide/pull/1680)) +* Python: Added XGROUP SETID command ([#1683](https://github.com/valkey-io/valkey-glide/pull/1683)) +* Python: Added FUNCTION LOAD command ([#1699](https://github.com/valkey-io/valkey-glide/pull/1699)) +* Python: Added XPENDING command ([#1704](https://github.com/valkey-io/valkey-glide/pull/1704)) +* Python: Added RANDOMKEY command ([#1701](https://github.com/valkey-io/valkey-glide/pull/1701)) +* Python: Added FUNCTION FLUSH command ([#1700](https://github.com/valkey-io/valkey-glide/pull/1700)) +* Python: Added FUNCTION DELETE command ([#1714](https://github.com/valkey-io/valkey-glide/pull/1714)) +* Python: Added FUNCTION LIST command ([#1738](https://github.com/valkey-io/valkey-glide/pull/1738)) +* Python: Added SSCAN command ([#1709](https://github.com/valkey-io/valkey-glide/pull/1709)) +* Python: Added LCS command ([#1716](https://github.com/valkey-io/valkey-glide/pull/1716)) +* Python: Added WAIT command ([#1710](https://github.com/valkey-io/valkey-glide/pull/1710)) +* Python: Added XAUTOCLAIM command ([#1718](https://github.com/valkey-io/valkey-glide/pull/1718)) +* Python: Add ZSCAN and HSCAN commands ([#1732](https://github.com/valkey-io/valkey-glide/pull/1732)) +* Python: Added FCALL_RO command ([#1721](https://github.com/valkey-io/valkey-glide/pull/1721)) +* Python: Added WATCH and UNWATCH command ([#1736](https://github.com/valkey-io/valkey-glide/pull/1736)) +* Python: Added XCLAIM command ([#1772](https://github.com/valkey-io/valkey-glide/pull/1772)) +* Python: Added XINFO GROUPS and XINFO CONSUMERS commands ([#1753](https://github.com/valkey-io/valkey-glide/pull/1753)) +* Python: Added LPOS command ([#1740](https://github.com/valkey-io/valkey-glide/pull/1740)) +* Python: Added SCAN command ([#1623](https://github.com/valkey-io/valkey-glide/pull/1623)) +* Python: Added DUMP and Restore commands ([#1733](https://github.com/valkey-io/valkey-glide/pull/1733)) +* Java: Added SCAN command ([#1751](https://github.com/valkey-io/valkey-glide/pull/1751)) +* Python: Added FUNCTION KILL command ([#1797](https://github.com/valkey-io/valkey-glide/pull/1797)) +* Python: Type migration for entries_read ([#1768](https://github.com/valkey-io/valkey-glide/pull/1768)) +* Python: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#1769](https://github.com/valkey-io/valkey-glide/pull/1769)) +* Python: Added FUNCTION STATS command ([#1794](https://github.com/valkey-io/valkey-glide/pull/1794)) +* Python: Added XINFO STREAM command ([#1816](https://github.com/valkey-io/valkey-glide/pull/1816)) +* Python: Added transaction supports for DUMP, RESTORE, FUNCTION DUMP and FUNCTION RESTORE ([#1814](https://github.com/valkey-io/valkey-glide/pull/1814)) +* Node: Added FlushAll command ([#1958](https://github.com/valkey-io/valkey-glide/pull/1958)) +* Node: Added DBSize command ([#1932](https://github.com/valkey-io/valkey-glide/pull/1932)) + +#### Breaking Changes +* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/valkey-io/valkey-glide/pull/1494)) +* Node: Rename RedisClient to GlideClient and RedisClusterClient to GlideClusterClient ([#1670](https://github.com/valkey-io/valkey-glide/pull/1670)) +* Python: Rename RedisClient to GlideClient, RedisClusterClient to GlideClusterClient and BaseRedisClient to BaseClient([#1669](https://github.com/valkey-io/valkey-glide/pull/1669)) +* Python: Rename ClusterClientConfiguration to GlideClusterClientConfiguration ([#1806](https://github.com/valkey-io/valkey-glide/pull/1806)) #### Fixes -* Python: Fix typing error "‘type’ object is not subscriptable" ([#1203](https://github.com/aws/glide-for-redis/pull/1203)) +* Python: fixing a bug with transaction exec ([#1796](https://github.com/valkey-io/valkey-glide/pull/1796)) + +## 0.4.1 (2024-06-02) + +#### Fixes +* Node: Fix set command bug with expiry option ([#1508](https://github.com/valkey-io/valkey-glide/pull/1508)) + +## 0.4.0 (2024-05-26) + +#### Changes +* Python: Added JSON.DEL JSON.FORGET commands ([#1146](https://github.com/valkey-io/valkey-glide/pull/1146)) +* Python: Added STRLEN command ([#1230](https://github.com/valkey-io/valkey-glide/pull/1230)) +* Python: Added HKEYS command ([#1228](https://github.com/valkey-io/valkey-glide/pull/1228)) +* Python: Added RPUSHX and LPUSHX commands ([#1239](https://github.com/valkey-io/valkey-glide/pull/1239)) +* Python: Added ZREMRANGEBYSCORE command ([#1151](https://github.com/valkey-io/valkey-glide/pull/1151)) +* Node, Python: Added SPOP, SPOPCOUNT commands. ([#1117](https://github.com/valkey-io/valkey-glide/pull/1117), [#1261](https://github.com/valkey-io/valkey-glide/pull/1261)) +* Node: Added ZRANGE command ([#1115](https://github.com/valkey-io/valkey-glide/pull/1115)) +* Python: Added RENAME command ([#1252](https://github.com/valkey-io/valkey-glide/pull/1252)) +* Python: Added APPEND command ([#1152](https://github.com/valkey-io/valkey-glide/pull/1152)) +* Python: Added GEOADD command ([#1259](https://github.com/valkey-io/valkey-glide/pull/1259)) +* Python: Added GEODIST command ([#1260](https://github.com/valkey-io/valkey-glide/pull/1260)) +* Python: Added GEOHASH command ([#1281](https://github.com/valkey-io/valkey-glide/pull/1281)) +* Python: Added ZLEXCOUNT command ([#1305](https://github.com/valkey-io/valkey-glide/pull/1305)) +* Python: Added ZREMRANGEBYLEX command ([#1306](https://github.com/valkey-io/valkey-glide/pull/1306)) +* Python: Added LINSERT command ([#1304](https://github.com/valkey-io/valkey-glide/pull/1304)) +* Python: Added GEOPOS command ([#1301](https://github.com/valkey-io/valkey-glide/pull/1301)) +* Node: Added PFADD command ([#1317](https://github.com/valkey-io/valkey-glide/pull/1317)) +* Python: Added PFADD command ([#1315](https://github.com/valkey-io/valkey-glide/pull/1315)) +* Python: Added ZMSCORE command ([#1357](https://github.com/valkey-io/valkey-glide/pull/1357)) +* Python: Added HRANDFIELD command ([#1334](https://github.com/valkey-io/valkey-glide/pull/1334)) +* Node: Added BLPOP command ([#1223](https://github.com/valkey-io/valkey-glide/pull/1223)) +* Python: Added XADD, XTRIM commands ([#1320](https://github.com/valkey-io/valkey-glide/pull/1320)) +* Python: Added BLPOP and BRPOP commands ([#1369](https://github.com/valkey-io/valkey-glide/pull/1369)) +* Python: Added ZRANGESTORE command ([#1377](https://github.com/valkey-io/valkey-glide/pull/1377)) +* Python: Added ZDIFFSTORE command ([#1378](https://github.com/valkey-io/valkey-glide/pull/1378)) +* Python: Added ZDIFF command ([#1401](https://github.com/valkey-io/valkey-glide/pull/1401)) +* Python: Added BZPOPMIN and BZPOPMAX commands ([#1399](https://github.com/valkey-io/valkey-glide/pull/1399)) +* Python: Added ZUNIONSTORE, ZINTERSTORE commands ([#1388](https://github.com/valkey-io/valkey-glide/pull/1388)) +* Python: Added ZRANDMEMBER command ([#1413](https://github.com/valkey-io/valkey-glide/pull/1413)) +* Python: Added BZMPOP command ([#1412](https://github.com/valkey-io/valkey-glide/pull/1412)) +* Python: Added ZINTERCARD command ([#1418](https://github.com/valkey-io/valkey-glide/pull/1418)) +* Python: Added ZMPOP command ([#1417](https://github.com/valkey-io/valkey-glide/pull/1417)) +* Python: Added SMOVE command ([#1421](https://github.com/valkey-io/valkey-glide/pull/1421)) +* Python: Added SUNIONSTORE command ([#1423](https://github.com/valkey-io/valkey-glide/pull/1423)) +* Python: Added SINTER command ([#1434](https://github.com/valkey-io/valkey-glide/pull/1434)) +* Python: Added SDIFF command ([#1437](https://github.com/valkey-io/valkey-glide/pull/1437)) +* Python: Added SDIFFSTORE command ([#1449](https://github.com/valkey-io/valkey-glide/pull/1449)) +* Python: Added SINTERSTORE command ([#1459](https://github.com/valkey-io/valkey-glide/pull/1459)) +* Python: Added SMISMEMBER command ([#1461](https://github.com/valkey-io/valkey-glide/pull/1461)) +* Python: Added SETRANGE command ([#1453](https://github.com/valkey-io/valkey-glide/pull/1453)) + +#### Fixes +* Python: Fix typing error "‘type’ object is not subscriptable" ([#1203](https://github.com/valkey-io/valkey-glide/pull/1203)) +* Core: Fixed blocking commands to use the specified timeout from the command argument ([#1283](https://github.com/valkey-io/valkey-glide/pull/1283)) + +### Breaking Changes +* Node: Changed `smembers` and `spopCount` functions to return Set instead of string[] ([#1299](https://github.com/valkey-io/valkey-glide/pull/1299)) + +#### Features +* Node: Added support for alpine based platform (Or any x64-musl or arm64-musl based platforms) ([#1379](https://github.com/valkey-io/valkey-glide/pull/1379)) ## 0.3.3 (2024-03-28) #### Fixes -* Node: Fix issue with dual usage, `CommonJS` and `ECMAScript` modules. ([#1199](https://github.com/aws/glide-for-redis/pull/1199)) +* Node: Fix issue with dual usage, `CommonJS` and `ECMAScript` modules. ([#1199](https://github.com/valkey-io/valkey-glide/pull/1199)) ## 0.3.0 (2024-03-25) #### Changes -* Python Node: Allow routing Cluster requests by address. ([#1021](https://github.com/aws/glide-for-redis/pull/1021)) -* Python, Node: Added HSETNX command. ([#954](https://github.com/aws/glide-for-redis/pull/954), [#1091](https://github.com/aws/glide-for-redis/pull/1091)) -* Python, Node: Added SISMEMBER command ([#972](https://github.com/aws/glide-for-redis/pull/972), [#1083](https://github.com/aws/glide-for-redis/pull/1083)) -* Python, Node: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945), [#980](https://github.com/aws/glide-for-redis/pull/980)) -* Python, Node: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944), [#981](https://github.com/aws/glide-for-redis/pull/981)) -* Python, Node: Added ZCOUNT command ([#878](https://github.com/aws/glide-for-redis/pull/878)) ([#909](https://github.com/aws/glide-for-redis/pull/909)) -* Python, Node: Added ECHO command ([#953](https://github.com/aws/glide-for-redis/pull/953), [#1010](https://github.com/aws/glide-for-redis/pull/1010)) -* Python, Node: Added ZPOPMIN command ([#975](https://github.com/aws/glide-for-redis/pull/975), [#1008](https://github.com/aws/glide-for-redis/pull/1008)) -* Node: Added STRLEN command ([#993](https://github.com/aws/glide-for-redis/pull/993)) -* Node: Added LINDEX command ([#999](https://github.com/aws/glide-for-redis/pull/999)) -* Python, Node: Added ZPOPMAX command ([#996](https://github.com/aws/glide-for-redis/pull/996), [#1009](https://github.com/aws/glide-for-redis/pull/1009)) -* Python: Added ZRANGE command ([#906](https://github.com/aws/glide-for-redis/pull/906)) -* Python, Node: Added PTTL command ([#1036](https://github.com/aws/glide-for-redis/pull/1036), [#1082](https://github.com/aws/glide-for-redis/pull/1082)) -* Python, Node: Added HVAL command ([#1130](https://github.com/aws/glide-for-redis/pull/1130)), ([#1022](https://github.com/aws/glide-for-redis/pull/1022)) -* Python, Node: Added PERSIST command ([#1129](https://github.com/aws/glide-for-redis/pull/1129)), ([#1023](https://github.com/aws/glide-for-redis/pull/1023)) -* Node: Added ZREMRANGEBYSCORE command ([#926](https://github.com/aws/glide-for-redis/pull/926)) -* Node: Added ZREMRANGEBYRANK command ([#924](https://github.com/aws/glide-for-redis/pull/924)) -* Node: Added Xadd, Xtrim commands. ([#1057](https://github.com/aws/glide-for-redis/pull/1057)) -* Python: Added json module and JSON.SET JSON.GET commands ([#1056](https://github.com/aws/glide-for-redis/pull/1056)) -* Python, Node: Added Time command ([#1147](https://github.com/aws/glide-for-redis/pull/1147)), ([#1114](https://github.com/aws/glide-for-redis/pull/1114)) -* Python, Node: Added LINDEX command ([#1058](https://github.com/aws/glide-for-redis/pull/1058), [#999](https://github.com/aws/glide-for-redis/pull/999)) -* Python, Node: Added ZRANK command ([#1065](https://github.com/aws/glide-for-redis/pull/1065), [#1149](https://github.com/aws/glide-for-redis/pull/1149)) -* Core: Enabled Cluster Mode periodic checks by default ([#1089](https://github.com/aws/glide-for-redis/pull/1089)) -* Node: Added Rename command. ([#1124](https://github.com/aws/glide-for-redis/pull/1124)) -* Python: Added JSON.TOGGLE command ([#1184](https://github.com/aws/glide-for-redis/pull/1184)) +* Python Node: Allow routing Cluster requests by address. ([#1021](https://github.com/valkey-io/valkey-glide/pull/1021)) +* Python, Node: Added HSETNX command. ([#954](https://github.com/valkey-io/valkey-glide/pull/954), [#1091](https://github.com/valkey-io/valkey-glide/pull/1091)) +* Python, Node: Added SISMEMBER command ([#972](https://github.com/valkey-io/valkey-glide/pull/972), [#1083](https://github.com/valkey-io/valkey-glide/pull/1083)) +* Python, Node: Added TYPE command ([#945](https://github.com/valkey-io/valkey-glide/pull/945), [#980](https://github.com/valkey-io/valkey-glide/pull/980)) +* Python, Node: Added HLEN command ([#944](https://github.com/valkey-io/valkey-glide/pull/944), [#981](https://github.com/valkey-io/valkey-glide/pull/981)) +* Python, Node: Added ZCOUNT command ([#878](https://github.com/valkey-io/valkey-glide/pull/878)) ([#909](https://github.com/valkey-io/valkey-glide/pull/909)) +* Python, Node: Added ECHO command ([#953](https://github.com/valkey-io/valkey-glide/pull/953), [#1010](https://github.com/valkey-io/valkey-glide/pull/1010)) +* Python, Node: Added ZPOPMIN command ([#975](https://github.com/valkey-io/valkey-glide/pull/975), [#1008](https://github.com/valkey-io/valkey-glide/pull/1008)) +* Node: Added STRLEN command ([#993](https://github.com/valkey-io/valkey-glide/pull/993)) +* Node: Added LINDEX command ([#999](https://github.com/valkey-io/valkey-glide/pull/999)) +* Python, Node: Added ZPOPMAX command ([#996](https://github.com/valkey-io/valkey-glide/pull/996), [#1009](https://github.com/valkey-io/valkey-glide/pull/1009)) +* Python: Added ZRANGE command ([#906](https://github.com/valkey-io/valkey-glide/pull/906)) +* Python, Node: Added PTTL command ([#1036](https://github.com/valkey-io/valkey-glide/pull/1036), [#1082](https://github.com/valkey-io/valkey-glide/pull/1082)) +* Python, Node: Added HVAL command ([#1130](https://github.com/valkey-io/valkey-glide/pull/1130)), ([#1022](https://github.com/valkey-io/valkey-glide/pull/1022)) +* Python, Node: Added PERSIST command ([#1129](https://github.com/valkey-io/valkey-glide/pull/1129)), ([#1023](https://github.com/valkey-io/valkey-glide/pull/1023)) +* Node: Added ZREMRANGEBYSCORE command ([#926](https://github.com/valkey-io/valkey-glide/pull/926)) +* Node: Added ZREMRANGEBYRANK command ([#924](https://github.com/valkey-io/valkey-glide/pull/924)) +* Node: Added Xadd, Xtrim commands. ([#1057](https://github.com/valkey-io/valkey-glide/pull/1057)) +* Python: Added json module and JSON.SET JSON.GET commands ([#1056](https://github.com/valkey-io/valkey-glide/pull/1056)) +* Python, Node: Added Time command ([#1147](https://github.com/valkey-io/valkey-glide/pull/1147)), ([#1114](https://github.com/valkey-io/valkey-glide/pull/1114)) +* Python, Node: Added LINDEX command ([#1058](https://github.com/valkey-io/valkey-glide/pull/1058), [#999](https://github.com/valkey-io/valkey-glide/pull/999)) +* Python, Node: Added ZRANK command ([#1065](https://github.com/valkey-io/valkey-glide/pull/1065), [#1149](https://github.com/valkey-io/valkey-glide/pull/1149)) +* Core: Enabled Cluster Mode periodic checks by default ([#1089](https://github.com/valkey-io/valkey-glide/pull/1089)) +* Node: Added Rename command. ([#1124](https://github.com/valkey-io/valkey-glide/pull/1124)) +* Python: Added JSON.TOGGLE command ([#1184](https://github.com/valkey-io/valkey-glide/pull/1184)) #### Features -* Python: Allow chaining function calls on transaction. ([#987](https://github.com/aws/glide-for-redis/pull/987)) -* Node: Adding support for GLIDE's usage in projects based on either `CommonJS` or `ECMAScript` modules. ([#1132](https://github.com/aws/glide-for-redis/pull/1132)) -* Python, Node: Added Cluster Mode configuration for periodic checks interval ([#1089](https://github.com/aws/glide-for-redis/pull/1089), [#1158](https://github.com/aws/glide-for-redis/pull/1158)) +* Python: Allow chaining function calls on transaction. ([#987](https://github.com/valkey-io/valkey-glide/pull/987)) +* Node: Adding support for GLIDE's usage in projects based on either `CommonJS` or `ECMAScript` modules. ([#1132](https://github.com/valkey-io/valkey-glide/pull/1132)) +* Python, Node: Added Cluster Mode configuration for periodic checks interval ([#1089](https://github.com/valkey-io/valkey-glide/pull/1089), [#1158](https://github.com/valkey-io/valkey-glide/pull/1158)) ## 0.2.0 (2024-02-11) #### Changes -* Python, Node: Added ZCARD command ([#871](https://github.com/aws/glide-for-redis/pull/871), [#885](https://github.com/aws/glide-for-redis/pull/885)) -* Python, Node: Added ZADD and ZADDINCR commands ([#814](https://github.com/aws/glide-for-redis/pull/814), [#830](https://github.com/aws/glide-for-redis/pull/830)) -* Python, Node: Added ZREM command ([#834](https://github.com/aws/glide-for-redis/pull/834), [#831](https://github.com/aws/glide-for-redis/pull/831)) -* Python, Node: Added ZSCORE command ([#877](https://github.com/aws/glide-for-redis/pull/877), [#889](https://github.com/aws/glide-for-redis/pull/889)) -* Use jemalloc as default allocator. ([#847](https://github.com/aws/glide-for-redis/pull/847)) -* Python, Node: Added RPOPCOUNT and LPOPCOUNT to transaction ([#874](https://github.com/aws/glide-for-redis/pull/874)) -* Standalone client: Improve connection errors. ([#854](https://github.com/aws/glide-for-redis/pull/854)) -* Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/aws/glide-for-redis/pull/811)) -* Python, Node: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945), [#980](https://github.com/aws/glide-for-redis/pull/980)) -* Python, Node: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944), [#981](https://github.com/aws/glide-for-redis/pull/981)) -* Python, Node: Added ZCOUNT command ([#878](https://github.com/aws/glide-for-redis/pull/878)) ([#909](https://github.com/aws/glide-for-redis/pull/909)) -* Python: Added ECHO command ([#953](https://github.com/aws/glide-for-redis/pull/953)) -* Python, Node: Added ZPOPMIN command ([#975](https://github.com/aws/glide-for-redis/pull/975), [#1008](https://github.com/aws/glide-for-redis/pull/1008)) -* Node: Added STRLEN command ([#993](https://github.com/aws/glide-for-redis/pull/993)) -* Node: Added LINDEX command ([#999](https://github.com/aws/glide-for-redis/pull/999)) -* Python, Node: Added ZPOPMAX command ([#996](https://github.com/aws/glide-for-redis/pull/996), [#1009](https://github.com/aws/glide-for-redis/pull/1009)) -* Python: Added DBSIZE command ([#1040](https://github.com/aws/glide-for-redis/pull/1040)) +* Python, Node: Added ZCARD command ([#871](https://github.com/valkey-io/valkey-glide/pull/871), [#885](https://github.com/valkey-io/valkey-glide/pull/885)) +* Python, Node: Added ZADD and ZADDINCR commands ([#814](https://github.com/valkey-io/valkey-glide/pull/814), [#830](https://github.com/valkey-io/valkey-glide/pull/830)) +* Python, Node: Added ZREM command ([#834](https://github.com/valkey-io/valkey-glide/pull/834), [#831](https://github.com/valkey-io/valkey-glide/pull/831)) +* Python, Node: Added ZSCORE command ([#877](https://github.com/valkey-io/valkey-glide/pull/877), [#889](https://github.com/valkey-io/valkey-glide/pull/889)) +* Use jemalloc as default allocator. ([#847](https://github.com/valkey-io/valkey-glide/pull/847)) +* Python, Node: Added RPOPCOUNT and LPOPCOUNT to transaction ([#874](https://github.com/valkey-io/valkey-glide/pull/874)) +* Standalone client: Improve connection errors. ([#854](https://github.com/valkey-io/valkey-glide/pull/854)) +* Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/valkey-io/valkey-glide/pull/811)) +* Python, Node: Added TYPE command ([#945](https://github.com/valkey-io/valkey-glide/pull/945), [#980](https://github.com/valkey-io/valkey-glide/pull/980)) +* Python, Node: Added HLEN command ([#944](https://github.com/valkey-io/valkey-glide/pull/944), [#981](https://github.com/valkey-io/valkey-glide/pull/981)) +* Python, Node: Added ZCOUNT command ([#878](https://github.com/valkey-io/valkey-glide/pull/878)) ([#909](https://github.com/valkey-io/valkey-glide/pull/909)) +* Python: Added ECHO command ([#953](https://github.com/valkey-io/valkey-glide/pull/953)) +* Python, Node: Added ZPOPMIN command ([#975](https://github.com/valkey-io/valkey-glide/pull/975), [#1008](https://github.com/valkey-io/valkey-glide/pull/1008)) +* Node: Added STRLEN command ([#993](https://github.com/valkey-io/valkey-glide/pull/993)) +* Node: Added LINDEX command ([#999](https://github.com/valkey-io/valkey-glide/pull/999)) +* Python, Node: Added ZPOPMAX command ([#996](https://github.com/valkey-io/valkey-glide/pull/996), [#1009](https://github.com/valkey-io/valkey-glide/pull/1009)) +* Python: Added DBSIZE command ([#1040](https://github.com/valkey-io/valkey-glide/pull/1040)) #### Features -* Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860)) -* Node: Allow chaining function calls on transaction. ([#902](https://github.com/aws/glide-for-redis/pull/902)) +* Python, Node: Added support in Lua Scripts ([#775](https://github.com/valkey-io/valkey-glide/pull/775), [#860](https://github.com/valkey-io/valkey-glide/pull/860)) +* Node: Allow chaining function calls on transaction. ([#902](https://github.com/valkey-io/valkey-glide/pull/902)) #### Fixes -* Core: Fixed `Connection Refused` error not to close the client ([#872](https://github.com/aws/glide-for-redis/pull/872)) -* Socket listener: fix identifier for closed reader error. ([#853](https://github.com/aws/glide-for-redis/pull/853)) -* Node: Fix issues with type import & exports ([#767](https://github.com/aws/glide-for-redis/pull/767)) +* Core: Fixed `Connection Refused` error not to close the client ([#872](https://github.com/valkey-io/valkey-glide/pull/872)) +* Socket listener: fix identifier for closed reader error. ([#853](https://github.com/valkey-io/valkey-glide/pull/853)) +* Node: Fix issues with type import & exports ([#767](https://github.com/valkey-io/valkey-glide/pull/767)) * Core: Added handling to "?" and NULL hostnames in CLUSTER SLOTS ([#104](https://github.com/amazon-contributing/redis-rs/pull/104)) * Core: Cluster connection now reconnects after full disconnect. ([#100](https://github.com/amazon-contributing/redis-rs/pull/100)) diff --git a/CODEOWNERS b/CODEOWNERS index 3dc9aa40c2..f578fe05e8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @aws/glide +* @valkey-io/glide-maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6f42f3af5..27e201d5f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,7 @@ GitHub provides additional document on [forking a repository](https://help.githu Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. ## Developer Guides +- [Java](./java/DEVELOPER.md) - [Node](./node/DEVELOPER.md) - [Python](./python/DEVELOPER.md) @@ -48,7 +49,7 @@ opensource-codeofconduct@amazon.com with any additional questions or comments. ## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. +See [SECURITY.md](./SECURITY.md) ## Licensing diff --git a/NOTICE b/NOTICE index d6d92d6665..750a796016 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ -A polyglot Redis client +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. redis-rs code copyright 2022 by redis-rs contributors. diff --git a/README.md b/README.md index caf6f9eeb4..4ccfd8594d 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,36 @@ -# GLIDE for Redis -General Language Independent Driver for the Enterprise (GLIDE) for Redis, is an AWS-sponsored, open-source Redis client. GLIDE for Redis works with any Redis distribution that adheres to the Redis Serialization Protocol (RESP) specification, including open-source Redis, Amazon ElastiCache for Redis, and Amazon MemoryDB for Redis. -Strategic, mission-critical Redis-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Redis is designed to provide a client experience that helps meet these objectives. It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Redis-compatible services used by hundreds of thousands of customers. To help ensure consistency in development and operations, GLIDE for Redis is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. In this Preview release, GLIDE for Redis is available for Python and Javascript (Node.js), with support for Java actively under development. +# Valkey GLIDE +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. -## Supported Redis Versions -GLIDE for Redis is API-compatible with open source Redis version 6 and 7. +## Supported Engine Versions +Valkey GLIDE is API-compatible with the following engine versions: -## Current Status -We've made GLIDE for Redis an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. -This preview release is recommended for testing purposes only. It is available in Python and Javascript (Node.js), with Java to follow. We're tracking its production readiness and future features on the [roadmap](https://github.com/orgs/aws/projects/187/). +| Engine Type | 6.2 | 7.0 | 7.2 | +|-----------------------|-------|-------|-------| +| Valkey | - | - | V | +| Redis | V | V | V | +## Current Status +In this release, Valkey GLIDE is available for Python and Java. Support for Node.js is actively under development, with plans to include more programming languages in the future. We're tracking future features on the [roadmap](https://github.com/orgs/valkey-io/projects/11). ## Getting Started - -- [Node](./node/README.md) +- [Java](./java/README.md) - [Python](./python/README.md) -- [Documentation](https://github.com/aws/glide-for-redis/wiki) +- [Documentation](https://github.com/valkey-io/valkey-glide/wiki) ## Getting Help -If you have any questions, feature requests, encounter issues, or need assistance with this project, please don't hesitate to open a GitHub issue. Our community and contributors are here to help you. Before creating an issue, we recommend checking the [existing issues](https://github.com/aws/glide-for-redis/issues) to see if your question or problem has already been addressed. If not, feel free to create a new issue, and we'll do our best to assist you. Please provide as much detail as possible in your issue description, including: +If you have any questions, feature requests, encounter issues, or need assistance with this project, please don't hesitate to open a GitHub issue. Our community and contributors are here to help you. Before creating an issue, we recommend checking the [existing issues](https://github.com/valkey-io/valkey-glide/issues) to see if your question or problem has already been addressed. If not, feel free to create a new issue, and we'll do our best to assist you. Please provide as much detail as possible in your issue description, including: 1. A clear and concise title 2. Detailed description of the problem or question -3. A reproducible test case or series of steps -4. The GLIDE for Redis version in use -5. Operating system -6. Redis version -7. Redis cluster information, cluster topology, number of shards, number of replicas, used data types -8. Any modifications you've made that are relevant to the issue -9. Anything unusual about your environment or deployment +3. Reproducible test case or step-by-step instructions +4. Valkey GLIDE version in use +5. Operating system details +6. Server version +7. Cluster or standalone setup information, including topology, number of shards, number of replicas, and data types used +8. Relevant modifications you've made +9. Any unusual aspects of your environment or deployment 10. Log files - ## Contributing GitHub is a platform for collaborative coding. If you're interested in writing code, we encourage you to contribute by submitting pull requests from forked copies of this repository. Additionally, please consider creating GitHub issues for reporting bugs and suggesting new features. Feel free to comment on issues that interest. For more info see [Contributing](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md index 75a3b51e55..d7f9b31a9c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,11 +1,6 @@ -## Reporting Security Issues +# Reporting a Vulnerability -We take all security reports seriously. -When we receive such reports, -we will investigate and subsequently address -any potential vulnerabilities as quickly as possible. -If you discover a potential security issue in this project, -please notify AWS/Amazon Security via our -[vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) -or directly via email to [AWS Security](mailto:aws-security@amazon.com). -Please do *not* create a public GitHub issue in this project. +If you believe you've discovered a security vulnerability, please contact the Valkey team at security@lists.valkey.io. +Please *DO NOT* create an issue. +We follow a responsible disclosure procedure, so depending on the severity of the issue we may notify Valkey vendors about the issue before releasing it publicly. +If you would like to be added to our list of vendors, please reach out to the Valkey team at valkey-glide@lists.valkey.io. diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs index 84a785aac5..3a3e8a6f08 100644 --- a/benchmarks/csharp/Program.cs +++ b/benchmarks/csharp/Program.cs @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Collections.Concurrent; using System.Diagnostics; diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index d3c34b59ea..5fb83801dc 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -33,6 +33,7 @@ chosenClients="all" host="localhost" port=6379 tlsFlag="--tls" +dotnetFramework="net6.0" function runPythonBenchmark(){ # generate protobuf files @@ -67,7 +68,7 @@ function runCSharpBenchmark(){ cd ${BENCH_FOLDER}/csharp dotnet clean dotnet build --configuration Release /warnaserror - dotnet run --framework net6.0 --configuration Release --resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --clientCount $clientCount $tlsFlag $portFlag $minimalFlag + dotnet run --framework $dotnetFramework --configuration Release --resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --clientCount $clientCount $tlsFlag $portFlag $minimalFlag } function runJavaBenchmark(){ @@ -139,6 +140,7 @@ function Help() { echo The benchmark will connect to the server using transport level security \(TLS\) by default. Pass -no-tls to connect to server without TLS. echo By default, the benchmark runs against localhost. Pass -host and then the address of the requested Redis server in order to connect to a different server. echo By default, the benchmark runs against port 6379. Pass -port and then the port number in order to connect to a different port. + echo By default, the C# benchmark runs with 'net6.0' framework. Pass -dotnet-framework and then the framework version in order to use a different framework. } while test $# -gt 0 @@ -227,6 +229,9 @@ do -minimal) minimalFlag="--minimal" ;; + -dotnet-framework) + dotnetFramework=$2 + ;; esac shift done diff --git a/benchmarks/node/node_benchmark.ts b/benchmarks/node/node_benchmark.ts index ce044cb770..9c7e0b9772 100644 --- a/benchmarks/node/node_benchmark.ts +++ b/benchmarks/node/node_benchmark.ts @@ -1,14 +1,14 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { writeFileSync } from "fs"; -import { Logger, RedisClient, RedisClusterClient } from "glide-for-redis"; import { Cluster, Redis } from "ioredis"; import { parse } from "path"; import percentile from "percentile"; import { RedisClientType, createClient, createCluster } from "redis"; import { stdev } from "stats-lite"; +import { GlideClient, GlideClusterClient, Logger } from "valkey-glide"; import { generateKeyGet, generateKeySet, @@ -216,8 +216,8 @@ async function main( if (clientsToRun == "all" || clientsToRun == "glide") { const clientClass = clusterModeEnabled - ? RedisClusterClient - : RedisClient; + ? GlideClusterClient + : GlideClient; const clients = await createClients(clientCount, () => clientClass.createClient({ addresses: [{ host, port }], @@ -232,7 +232,7 @@ async function main( dataSize, data, (client) => { - (client as RedisClient).close(); + (client as GlideClient).close(); }, clusterModeEnabled, ); @@ -240,11 +240,11 @@ async function main( } if (clientsToRun == "all") { - const nodeRedisClients = await createClients(clientCount, async () => { + const nodeGlideClients = await createClients(clientCount, async () => { const node = { url: getAddress(host, useTLS, port), }; - const nodeRedisClient = clusterModeEnabled + const nodeGlideClient = clusterModeEnabled ? createCluster({ rootNodes: [{ socket: { host, port, tls: useTLS } }], defaults: { @@ -255,11 +255,11 @@ async function main( useReplicas: true, }) : createClient(node); - await nodeRedisClient.connect(); - return nodeRedisClient; + await nodeGlideClient.connect(); + return nodeGlideClient; }); await runClients( - nodeRedisClients, + nodeGlideClients, "node_redis", totalCommands, numOfConcurrentTasks, diff --git a/benchmarks/node/package.json b/benchmarks/node/package.json index e1e2e27f7a..4ee1a76728 100644 --- a/benchmarks/node/package.json +++ b/benchmarks/node/package.json @@ -14,7 +14,7 @@ "@types/command-line-args": "^5.2.0", "@types/stats-lite": "^2.2.0", "command-line-args": "^5.2.1", - "glide-for-redis": "file:../../node", + "valkey-glide": "file:../../node", "ioredis": "5.3.2", "percentile": "^1.6.0", "redis": "4.6.13", diff --git a/benchmarks/python/python_benchmark.py b/benchmarks/python/python_benchmark.py index 29262764eb..1da52f9941 100644 --- a/benchmarks/python/python_benchmark.py +++ b/benchmarks/python/python_benchmark.py @@ -1,4 +1,4 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import argparse import asyncio @@ -17,11 +17,11 @@ import redis.asyncio as redispy # type: ignore from glide import ( BaseClientConfiguration, + GlideClient, + GlideClusterClient, Logger, LogLevel, NodeAddress, - RedisClient, - RedisClusterClient, ) @@ -288,7 +288,7 @@ async def main( if clients_to_run == "all" or clients_to_run == "glide": # Glide Socket - client_class = RedisClusterClient if is_cluster else RedisClient + client_class = GlideClusterClient if is_cluster else GlideClient config = BaseClientConfiguration( [NodeAddress(host=host, port=port)], use_tls=use_tls ) diff --git a/benchmarks/rust/Cargo.toml b/benchmarks/rust/Cargo.toml index 7247c082af..6f0849d505 100644 --- a/benchmarks/rust/Cargo.toml +++ b/benchmarks/rust/Cargo.toml @@ -3,7 +3,7 @@ name = "rust-benchmark" version = "0.1.0" edition = "2021" license = "Apache 2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/benchmarks/rust/src/main.rs b/benchmarks/rust/src/main.rs index 8503375195..edace91a30 100644 --- a/benchmarks/rust/src/main.rs +++ b/benchmarks/rust/src/main.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ #[cfg(not(target_env = "msvc"))] @@ -236,7 +236,7 @@ async fn get_connection(args: &Args) -> Client { ..Default::default() }; - glide_core::client::Client::new(connection_request) + glide_core::client::Client::new(connection_request, None) .await .unwrap() } diff --git a/benchmarks/utilities/csv_exporter.py b/benchmarks/utilities/csv_exporter.py index 080aa22e4f..2841e867f6 100755 --- a/benchmarks/utilities/csv_exporter.py +++ b/benchmarks/utilities/csv_exporter.py @@ -1,6 +1,6 @@ #!/bin/python3 -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import csv import json diff --git a/benchmarks/utilities/fill_db.ts b/benchmarks/utilities/fill_db.ts index 45c1412e02..01bd29884f 100644 --- a/benchmarks/utilities/fill_db.ts +++ b/benchmarks/utilities/fill_db.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { diff --git a/benchmarks/utilities/flush_db.ts b/benchmarks/utilities/flush_db.ts index b5a59cc0f2..00d2af086f 100644 --- a/benchmarks/utilities/flush_db.ts +++ b/benchmarks/utilities/flush_db.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { RedisClientType, RedisClusterType } from "redis"; diff --git a/benchmarks/utilities/utils.ts b/benchmarks/utilities/utils.ts index 140dd4fadd..3e1c2e8014 100644 --- a/benchmarks/utilities/utils.ts +++ b/benchmarks/utilities/utils.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import commandLineArgs from "command-line-args"; diff --git a/csharp/.editorconfig b/csharp/.editorconfig index 4a0f9f3bb6..d05fdf9728 100644 --- a/csharp/.editorconfig +++ b/csharp/.editorconfig @@ -7,7 +7,7 @@ indent_size = 2 [*.cs] # License header -file_header_template = Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +file_header_template = Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 # Organize usings dotnet_separate_import_directive_groups = true diff --git a/csharp/DEVELOPER.md b/csharp/DEVELOPER.md index 1042aae2e4..a5bb030474 100644 --- a/csharp/DEVELOPER.md +++ b/csharp/DEVELOPER.md @@ -83,8 +83,8 @@ Before starting this step, make sure you've installed all software requirments. ```bash VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch -git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git -cd glide-for-redis +git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git +cd valkey-glide ``` 2. Initialize git submodule diff --git a/csharp/README.md b/csharp/README.md index 5ebc9e1961..390b146adc 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -1,19 +1,18 @@ # C# wrapper -The C# wrapper is currently not in a usable state. +The C# wrapper is currently not in a usable state and is under development. -# GLIDE for Redis +# Valkey GLIDE -General Language Independent Driver for the Enterprise (GLIDE) for Redis, is an AWS-sponsored, open-source Redis client. GLIDE for Redis works with any Redis distribution that adheres to the Redis Serialization Protocol (RESP) specification, including open-source Redis, Amazon ElastiCache for Redis, and Amazon MemoryDB for Redis. -Strategic, mission-critical Redis-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Redis is designed to provide a client experience that helps meet these objectives. It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Redis-compatible services used by hundreds of thousands of customers. To help ensure consistency in development and operations, GLIDE for Redis is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. -## Supported Redis Versions +## Supported Engine Versions -GLIDE for Redis is API-compatible with open source Redis version 6 and 7. +Refer to the [Supported Engine Versions table](https://github.com/valkey-io/valkey-glide/blob/main/README.md#supported-engine-versions) for details. ## Current Status -We've made GLIDE for Redis an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. +We've made Valkey GLIDE an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. This preview release is recommended for testing purposes only. # Getting Started - C# Wrapper diff --git a/csharp/lib/AsyncClient.cs b/csharp/lib/AsyncClient.cs index a584a58af1..3e6aab1ba8 100644 --- a/csharp/lib/AsyncClient.cs +++ b/csharp/lib/AsyncClient.cs @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Buffers; using System.Runtime.InteropServices; @@ -38,7 +38,7 @@ public AsyncClient(string host, uint port, bool useTLS) IntPtr[] args = _arrayPool.Rent(2); args[0] = Marshal.StringToHGlobalAnsi(key); args[1] = Marshal.StringToHGlobalAnsi(value); - string? result = await Command(args, 2, RequestType.SetString); + string? result = await Command(args, 2, RequestType.Set); _arrayPool.Return(args); return result; } @@ -47,7 +47,7 @@ public AsyncClient(string host, uint port, bool useTLS) { IntPtr[] args = _arrayPool.Rent(1); args[0] = Marshal.StringToHGlobalAnsi(key); - string? result = await Command(args, 1, RequestType.GetString); + string? result = await Command(args, 1, RequestType.Get); _arrayPool.Return(args); return result; } @@ -129,8 +129,8 @@ private enum RequestType { InvalidRequest = 0, CustomCommand = 1, - GetString = 2, - SetString = 3, + Get = 2, + Set = 3, Ping = 4, Info = 5, Del = 6, @@ -154,10 +154,10 @@ private enum RequestType ClientUnblock = 24, ClientUnpause = 25, Expire = 26, - HashSet = 27, - HashGet = 28, - HashDel = 29, - HashExists = 30, + HSet = 27, + HGet = 28, + HDel = 29, + HExists = 30, MGet = 31, MSet = 32, Incr = 33, @@ -165,11 +165,11 @@ private enum RequestType Decr = 35, IncrByFloat = 36, DecrBy = 37, - HashGetAll = 38, - HashMSet = 39, - HashMGet = 40, - HashIncrBy = 41, - HashIncrByFloat = 42, + HGetAll = 38, + HMSet = 39, + HMGet = 40, + HIncrBy = 41, + HIncrByFloat = 42, LPush = 43, LPop = 44, RPush = 45, @@ -188,11 +188,11 @@ private enum RequestType Exists = 58, Unlink = 59, TTL = 60, - Zadd = 61, - Zrem = 62, - Zrange = 63, - Zcard = 64, - Zcount = 65, + ZAdd = 61, + ZRem = 62, + ZRange = 63, + ZCard = 64, + ZCount = 65, ZIncrBy = 66, ZScore = 67, Type = 68, @@ -200,7 +200,7 @@ private enum RequestType Echo = 70, ZPopMin = 71, Strlen = 72, - Lindex = 73, + LIndex = 73, ZPopMax = 74, XRead = 75, XAdd = 76, @@ -211,21 +211,21 @@ private enum RequestType XGroupDestroy = 81, HSetNX = 82, SIsMember = 83, - Hvals = 84, + HVals = 84, PTTL = 85, ZRemRangeByRank = 86, Persist = 87, ZRemRangeByScore = 88, Time = 89, - Zrank = 90, + ZRank = 90, Rename = 91, DBSize = 92, - Brpop = 93, - Hkeys = 94, + BRPop = 93, + HKeys = 94, PfAdd = 96, PfCount = 97, PfMerge = 98, - Blpop = 100, + BLPop = 100, RPushX = 102, LPushX = 103, } diff --git a/csharp/lib/Cargo.toml b/csharp/lib/Cargo.toml index 6bf4183914..95981480b2 100644 --- a/csharp/lib/Cargo.toml +++ b/csharp/lib/Cargo.toml @@ -3,7 +3,7 @@ name = "glide-rs" version = "0.1.0" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/csharp/lib/Logger.cs b/csharp/lib/Logger.cs index fc30584323..814737e649 100644 --- a/csharp/lib/Logger.cs +++ b/csharp/lib/Logger.cs @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Runtime.InteropServices; diff --git a/csharp/lib/Message.cs b/csharp/lib/Message.cs index fd6d9090f7..9e3cdd4d2e 100644 --- a/csharp/lib/Message.cs +++ b/csharp/lib/Message.cs @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; diff --git a/csharp/lib/MessageContainer.cs b/csharp/lib/MessageContainer.cs index 18073a62d2..d2baf6e2cb 100644 --- a/csharp/lib/MessageContainer.cs +++ b/csharp/lib/MessageContainer.cs @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Collections.Concurrent; diff --git a/csharp/lib/Properties/AssemblyInfo.cs b/csharp/lib/Properties/AssemblyInfo.cs index e7e05eb672..9ddad510f9 100644 --- a/csharp/lib/Properties/AssemblyInfo.cs +++ b/csharp/lib/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Runtime.CompilerServices; diff --git a/csharp/lib/src/lib.rs b/csharp/lib/src/lib.rs index fce015a376..73a4be8681 100644 --- a/csharp/lib/src/lib.rs +++ b/csharp/lib/src/lib.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use glide_core::client; use glide_core::client::Client as GlideClient; @@ -59,7 +59,7 @@ fn create_client_internal( .thread_name("GLIDE for Redis C# thread") .build()?; let _runtime_handle = runtime.enter(); - let client = runtime.block_on(GlideClient::new(request)).unwrap(); // TODO - handle errors. + let client = runtime.block_on(GlideClient::new(request, None)).unwrap(); // TODO - handle errors. Ok(Client { client, success_callback, diff --git a/csharp/tests/Integration/GetAndSet.cs b/csharp/tests/Integration/GetAndSet.cs index 79eb6ec50e..792741cf44 100644 --- a/csharp/tests/Integration/GetAndSet.cs +++ b/csharp/tests/Integration/GetAndSet.cs @@ -1,20 +1,24 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Runtime.InteropServices; +using FluentAssertions; + using Glide; using static Tests.Integration.IntegrationTestBase; namespace Tests.Integration; -public class GetAndSet +public class GetAndSet : IClassFixture { private async Task GetAndSetValues(AsyncClient client, string key, string value) { - string? setResult = await client.SetAsync(key, value); - Assert.That(setResult, Is.EqualTo("OK")); - string? result = await client.GetAsync(key); - Assert.That(result, Is.EqualTo(value)); + _ = (await client.SetAsync(key, value)) + .Should() + .Be("OK"); + _ = (await client.GetAsync(key)) + .Should() + .Be(value); } private async Task GetAndSetRandomValues(AsyncClient client) @@ -24,14 +28,14 @@ private async Task GetAndSetRandomValues(AsyncClient client) await GetAndSetValues(client, key, value); } - [Test] + [Fact] public async Task GetReturnsLastSet() { using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); await GetAndSetRandomValues(client); } - [Test] + [Fact] public async Task GetAndSetCanHandleNonASCIIUnicode() { using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); @@ -40,15 +44,16 @@ public async Task GetAndSetCanHandleNonASCIIUnicode() await GetAndSetValues(client, key, value); } - [Test] + [Fact] public async Task GetReturnsNull() { using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); - string? result = await client.GetAsync(Guid.NewGuid().ToString()); - Assert.That(result, Is.EqualTo(null)); + _ = (await client.GetAsync(Guid.NewGuid().ToString())) + .Should() + .BeNull(); } - [Test] + [Fact] public async Task GetReturnsEmptyString() { using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); @@ -57,13 +62,14 @@ public async Task GetReturnsEmptyString() await GetAndSetValues(client, key, value); } - [Test] + [Fact] public async Task HandleVeryLargeInput() { // TODO invesitage and fix if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Assert.Ignore("Flaky on MacOS"); + //"Flaky on MacOS" + return; } using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); @@ -80,13 +86,13 @@ public async Task HandleVeryLargeInput() // This test is slow and hardly a unit test, but it caught timing and releasing issues in the past, // so it's being kept. - [Test] + [Fact] public void ConcurrentOperationsWork() { // TODO investigate and fix if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Assert.Ignore("Flaky on MacOS"); + return; } using AsyncClient client = new("localhost", TestConfiguration.STANDALONE_PORTS[0], false); @@ -105,8 +111,9 @@ public void ConcurrentOperationsWork() } else { - string? result = await client.GetAsync(Guid.NewGuid().ToString()); - Assert.That(result, Is.EqualTo(null)); + _ = (await client.GetAsync(Guid.NewGuid().ToString())) + .Should() + .BeNull(); } } })); diff --git a/csharp/tests/Integration/IntegrationTestBase.cs b/csharp/tests/Integration/IntegrationTestBase.cs index c4a87756d0..10d9872c4f 100644 --- a/csharp/tests/Integration/IntegrationTestBase.cs +++ b/csharp/tests/Integration/IntegrationTestBase.cs @@ -1,12 +1,14 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using System.Diagnostics; +using Xunit.Abstractions; +using Xunit.Sdk; + // Note: All IT should be in the same namespace namespace Tests.Integration; -[SetUpFixture] -public class IntegrationTestBase +public class IntegrationTestBase : IDisposable { internal class TestConfiguration { @@ -15,13 +17,28 @@ internal class TestConfiguration public static Version REDIS_VERSION { get; internal set; } = new(); } - [OneTimeSetUp] - public void SetUp() + private readonly IMessageSink _diagnosticMessageSink; + + public IntegrationTestBase(IMessageSink diagnosticMessageSink) { + _diagnosticMessageSink = diagnosticMessageSink; + string? projectDir = Directory.GetCurrentDirectory(); + while (!(Path.GetFileName(projectDir) == "csharp" || projectDir == null)) + { + projectDir = Path.GetDirectoryName(projectDir); + } + + if (projectDir == null) + { + throw new FileNotFoundException("Can't detect the project dir. Are you running tests from `csharp` directory?"); + } + + _scriptDir = Path.Combine(projectDir, "..", "utils"); + // Stop all if weren't stopped on previous test run StopRedis(false); - // Delete dirs if stop failed due to https://github.com/aws/glide-for-redis/issues/849 + // Delete dirs if stop failed due to https://github.com/valkey-io/valkey-glide/issues/849 Directory.Delete(Path.Combine(_scriptDir, "clusters"), true); // Start cluster @@ -31,34 +48,19 @@ public void SetUp() // Get redis version TestConfiguration.REDIS_VERSION = GetRedisVersion(); - TestContext.Progress.WriteLine($"Cluster ports = {string.Join(',', TestConfiguration.CLUSTER_PORTS)}"); - TestContext.Progress.WriteLine($"Standalone ports = {string.Join(',', TestConfiguration.STANDALONE_PORTS)}"); - TestContext.Progress.WriteLine($"Redis version = {TestConfiguration.REDIS_VERSION}"); + TestConsoleWriteLine($"Cluster ports = {string.Join(',', TestConfiguration.CLUSTER_PORTS)}"); + TestConsoleWriteLine($"Standalone ports = {string.Join(',', TestConfiguration.STANDALONE_PORTS)}"); + TestConsoleWriteLine($"Redis version = {TestConfiguration.REDIS_VERSION}"); } - [OneTimeTearDown] - public void TearDown() => + public void Dispose() => // Stop all StopRedis(true); private readonly string _scriptDir; - // Nunit requires a public default constructor. These variables would be set in SetUp method. - public IntegrationTestBase() - { - string? projectDir = Directory.GetCurrentDirectory(); - while (!(Path.GetFileName(projectDir) == "csharp" || projectDir == null)) - { - projectDir = Path.GetDirectoryName(projectDir); - } - - if (projectDir == null) - { - throw new FileNotFoundException("Can't detect the project dir. Are you running tests from `csharp` directory?"); - } - - _scriptDir = Path.Combine(projectDir, "..", "utils"); - } + private void TestConsoleWriteLine(string message) => + _ = _diagnosticMessageSink.OnMessage(new DiagnosticMessage(message)); internal List StartRedis(bool cluster, bool tls = false, string? name = null) { @@ -92,7 +94,7 @@ private string RunClusterManager(string cmd, bool ignoreExitCode) string? output = script?.StandardOutput.ReadToEnd(); int? exit_code = script?.ExitCode; - TestContext.Progress.WriteLine($"cluster_manager.py stdout\n====\n{output}\n====\ncluster_manager.py stderr\n====\n{error}\n====\n"); + TestConsoleWriteLine($"cluster_manager.py stdout\n====\n{output}\n====\ncluster_manager.py stderr\n====\n{error}\n====\n"); return !ignoreExitCode && exit_code != 0 ? throw new ApplicationException($"cluster_manager.py script failed: exit code {exit_code}.") @@ -131,7 +133,10 @@ private static Version GetRedisVersion() proc?.WaitForExit(); string output = proc?.StandardOutput.ReadToEnd() ?? ""; + // Redis response: // Redis server v=7.2.3 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=7504b1fedf883f2 - return new Version(output.Split(" ")[2].Split("=")[1]); + // Valkey response: + // Server v=7.2.5 sha=26388270:0 malloc=jemalloc-5.3.0 bits=64 build=ea40bb1576e402d6 + return new Version(output.Split("v=")[1].Split(" ")[0]); } } diff --git a/csharp/tests/Usings.cs b/csharp/tests/Usings.cs index 71106ca711..a14d42be1a 100644 --- a/csharp/tests/Usings.cs +++ b/csharp/tests/Usings.cs @@ -1,3 +1,3 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -global using NUnit.Framework; +global using Xunit; diff --git a/csharp/tests/tests.csproj b/csharp/tests/tests.csproj index 3d991e1084..dac47a8e06 100644 --- a/csharp/tests/tests.csproj +++ b/csharp/tests/tests.csproj @@ -5,6 +5,8 @@ enable enable preview + false + true true false @@ -16,11 +18,17 @@ - - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/examples/java/README.md b/examples/java/README.md new file mode 100644 index 0000000000..0e46d8ba62 --- /dev/null +++ b/examples/java/README.md @@ -0,0 +1,18 @@ +## Run + +Ensure that you have an instance of Valkey running on "localhost" on "6379". Otherwise, update glide.examples.ExamplesApp with a configuration that matches your server settings. + +To run the example: +``` +cd valkey-glide/examples/java +./gradlew :run +``` + +You should expect to see the output: +``` +> Task :run +PING: PONG +PING(found you): found you +SET(apples, oranges): OK +GET(apples): oranges +``` diff --git a/java/examples/build.gradle b/examples/java/build.gradle similarity index 52% rename from java/examples/build.gradle rename to examples/java/build.gradle index 0e526d95e4..fa55ac434e 100644 --- a/java/examples/build.gradle +++ b/examples/java/build.gradle @@ -1,25 +1,20 @@ plugins { // Apply the application plugin to add support for building a CLI application in Java. id 'application' + id "com.google.osdetector" version "1.7.3" } repositories { // Use Maven Central for resolving dependencies. mavenCentral() + mavenLocal() } dependencies { - implementation project(':client') - - implementation 'redis.clients:jedis:4.4.3' - implementation 'io.lettuce:lettuce-core:6.2.6.RELEASE' - implementation 'commons-cli:commons-cli:1.5.0' + implementation "io.valkey:valkey-glide:1.0.1:${osdetector.classifier}" } -run.dependsOn ':client:buildRustRelease' - application { // Define the main class for the application. mainClass = 'glide.examples.ExamplesApp' - applicationDefaultJvmArgs = ['-Djava.library.path=../target/release'] } diff --git a/examples/java/gradle/wrapper/gradle-wrapper.jar b/examples/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..d64cd49177 Binary files /dev/null and b/examples/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/java/gradle/wrapper/gradle-wrapper.properties b/examples/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..1af9e0930b --- /dev/null +++ b/examples/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/java/gradlew b/examples/java/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/examples/java/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/java/gradlew.bat b/examples/java/gradlew.bat new file mode 100755 index 0000000000..6689b85bee --- /dev/null +++ b/examples/java/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/java/settings.gradle b/examples/java/settings.gradle new file mode 100644 index 0000000000..50d945eb0f --- /dev/null +++ b/examples/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'glide.examples' diff --git a/java/examples/src/main/java/glide/examples/ExamplesApp.java b/examples/java/src/main/java/glide/examples/ExamplesApp.java similarity index 75% rename from java/examples/src/main/java/glide/examples/ExamplesApp.java rename to examples/java/src/main/java/glide/examples/ExamplesApp.java index ea816f9632..4a686786eb 100644 --- a/java/examples/src/main/java/glide/examples/ExamplesApp.java +++ b/examples/java/src/main/java/glide/examples/ExamplesApp.java @@ -1,9 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.examples; -import glide.api.RedisClient; +import glide.api.GlideClient; +import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; import java.util.concurrent.ExecutionException; public class ExamplesApp { @@ -18,14 +18,13 @@ private static void runGlideExamples() { Integer port = 6379; boolean useSsl = false; - RedisClientConfiguration config = - RedisClientConfiguration.builder() + GlideClientConfiguration config = + GlideClientConfiguration.builder() .address(NodeAddress.builder().host(host).port(port).build()) .useTLS(useSsl) .build(); - try { - RedisClient client = RedisClient.CreateClient(config).get(); + try (GlideClient client = GlideClient.createClient(config).get()) { System.out.println("PING: " + client.ping().get()); System.out.println("PING(found you): " + client.ping("found you").get()); diff --git a/examples/node/README.MD b/examples/node/README.MD index 9e666ff66f..adea1790ed 100644 --- a/examples/node/README.MD +++ b/examples/node/README.MD @@ -21,12 +21,12 @@ npm i -g npm@8 ## Build To build GLIDE's Node client, run (on unix based systems): ``` -cd glide-for-redis/node +cd valkey-glide/node git submodule update --init --recursive npm install rm -rf build-ts npm run build:release -cd glide-for-redis/examples/node +cd valkey-glide/examples/node npm install npx tsc ``` @@ -34,6 +34,6 @@ npx tsc ## Run To run the example: ``` -cd glide-for-redis/examples/node +cd valkey-glide/examples/node node index.js ``` diff --git a/examples/node/index.ts b/examples/node/index.ts index ec4d5c9d51..372b1ff845 100644 --- a/examples/node/index.ts +++ b/examples/node/index.ts @@ -1,8 +1,8 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { Logger, RedisClient, RedisClusterClient } from "@aws/glide-for-redis"; +import { GlideClient, GlideClusterClient, Logger } from "@valkey/valkey-glide"; async function sendPingToNode() { // When in Redis is in standalone mode, add address of the primary node, and any replicas you'd like to be able to read from. @@ -12,8 +12,8 @@ async function sendPingToNode() { port: 6379, }, ]; - // Check `RedisClientConfiguration/ClusterClientConfiguration` for additional options. - const client = await RedisClient.createClient({ + // Check `GlideClientConfiguration/ClusterClientConfiguration` for additional options. + const client = await GlideClient.createClient({ addresses: addresses, // if the server uses TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. // useTLS: true, @@ -26,7 +26,7 @@ async function sendPingToNode() { client.close(); } -async function send_set_and_get(client: RedisClient | RedisClusterClient) { +async function send_set_and_get(client: GlideClient | GlideClusterClient) { const set_response = await client.set("foo", "bar"); console.log(`Set response is = ${set_response}`); const get_response = await client.get("foo"); @@ -41,8 +41,8 @@ async function sendPingToRandomNodeInCluster() { port: 6380, }, ]; - // Check `RedisClientConfiguration/ClusterClientConfiguration` for additional options. - const client = await RedisClusterClient.createClient({ + // Check `GlideClientConfiguration/ClusterClientConfiguration` for additional options. + const client = await GlideClusterClient.createClient({ addresses: addresses, // if the cluster nodes use TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. // useTLS: true, diff --git a/examples/node/package.json b/examples/node/package.json index 456f09b467..1521a6d8c4 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -1,7 +1,7 @@ { "type": "module", "dependencies": { - "@aws/glide-for-redis": "^0.1.0", + "@valkey/valkey-glide": "^1.0.0", "@types/node": "^20.4.8" }, "devDependencies": { diff --git a/examples/python/README.md b/examples/python/README.md index be0e79160f..d9e8960978 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -1,7 +1,7 @@ ## Run -To run the example or any other Python application utilizing GLIDE for Redis, activate the virtual environment that created by the 'Build' stage: ``` cd examples/python pip install -r requirements.txt -python3 client_example.py +python3 standalone_example.py +python3 cluster_example.py ``` diff --git a/examples/python/client_example.py b/examples/python/client_example.py deleted file mode 100755 index 7620348a5b..0000000000 --- a/examples/python/client_example.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - -import asyncio -from typing import Optional, Union - -from glide import ( - AllNodes, - BaseClientConfiguration, - Logger, - LogLevel, - NodeAddress, - RedisClient, - RedisClusterClient, -) - - -def set_console_logger(level: LogLevel = LogLevel.WARN): - Logger.set_logger_config(level) - - -def set_file_logger(level: LogLevel = LogLevel.WARN, file: Optional[str] = None): - if file is None: - from datetime import datetime, timezone - - curr_time = datetime.now(timezone.utc) - curr_time_str = curr_time.strftime("%Y-%m-%dT%H:%M:%SZ") - file = f"{curr_time_str}-glide.log" - Logger.set_logger_config(level, file) - - -async def send_set_and_get(client: Union[RedisClient, RedisClusterClient]): - set_response = await client.set("foo", "bar") - print(f"Set response is = {set_response}") - get_response = await client.get("foo") - print(f"Get response is = {get_response}") - - -async def test_standalone_client(host: str = "localhost", port: int = 6379): - # When in Redis is in standalone mode, add address of the primary node, - # and any replicas you'd like to be able to read from. - addresses = [NodeAddress(host, port)] - # Check `RedisClientConfiguration/ClusterClientConfiguration` for additional options. - config = BaseClientConfiguration( - addresses=addresses, - client_name="test_standalone_client" - # if the server use TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. - # use_tls=True - ) - client = await RedisClient.create(config) - - # Send SET and GET - await send_set_and_get(client) - # Send PING to the primary node - pong = await client.custom_command(["PING"]) - print(f"PONG response is = {pong}") - - -async def test_cluster_client(host: str = "localhost", port: int = 6379): - # When in Redis is cluster mode, add address of any nodes, and the client will find all nodes in the cluster. - addresses = [NodeAddress(host, port)] - # Check `RedisClientConfiguration/ClusterClientConfiguration` for additional options. - config = BaseClientConfiguration( - addresses=addresses, - client_name="test_cluster_client" - # if the cluster nodes use TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. - # use_tls=True - ) - client = await RedisClusterClient.create(config) - - # Send SET and GET - await send_set_and_get(client) - # Send PING to all primaries (according to Redis's PING request_policy) - pong = await client.custom_command(["PING"]) - print(f"PONG response is = {pong}") - # Send INFO REPLICATION with routing option to all nodes - info_repl_resps = await client.custom_command(["INFO", "REPLICATION"], AllNodes()) - print(f"INFO REPLICATION responses to all nodes are = {info_repl_resps}") - - -async def main(): - set_console_logger(LogLevel.DEBUG) - set_file_logger(LogLevel.DEBUG) - await test_standalone_client() - await test_cluster_client() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/python/cluster_example.py b/examples/python/cluster_example.py new file mode 100644 index 0000000000..e26986bc87 --- /dev/null +++ b/examples/python/cluster_example.py @@ -0,0 +1,136 @@ +import asyncio +from typing import List, Tuple + +from glide import ( + AllNodes, + ClosingError, + ConnectionError, + GlideClusterClient, + GlideClusterClientConfiguration, + InfoSection, + Logger, + LogLevel, + NodeAddress, + RequestError, + TimeoutError, +) + + +async def create_client( + nodes_list: List[Tuple[str, int]] = [("localhost", 6379)] +) -> GlideClusterClient: + """ + Creates and returns a GlideClusterClient instance. + + This function initializes a GlideClusterClient with the provided list of nodes. + The nodes_list may contain the address of one or more cluster nodes, and the + client will automatically discover all nodes in the cluster. + + Args: + nodes_list (List[Tuple[str, int]]): A list of tuples where each tuple + contains a host (str) and port (int). Defaults to [("localhost", 6379)]. + + Returns: + GlideClusterClient: An instance of GlideClusterClient connected to the discovered nodes. + """ + addresses = [NodeAddress(host, port) for host, port in nodes_list] + # Check `GlideClusterClientConfiguration` for additional options. + config = GlideClusterClientConfiguration( + addresses=addresses, + client_name="test_cluster_client", + # Enable this field if the servers are configured with TLS. + # use_tls=True + ) + return await GlideClusterClient.create(config) + + +async def app_logic(client: GlideClusterClient): + """ + Executes the main logic of the application, performing basic operations + such as SET, GET, PING, and INFO REPLICATION using the provided GlideClusterClient. + + Args: + client (GlideClusterClient): An instance of GlideClusterClient. + """ + # Send SET and GET + set_response = await client.set("foo", "bar") + Logger.log(LogLevel.INFO, "app", f"Set response is = {set_response!r}") + + get_response = await client.get("foo") + assert isinstance(get_response, bytes) + Logger.log(LogLevel.INFO, "app", f"Get response is = {get_response.decode()!r}") + + # Send PING to all primaries (according to Redis's PING request_policy) + pong = await client.ping() + Logger.log(LogLevel.INFO, "app", f"PING response is = {pong!r}") + + # Send INFO REPLICATION with routing option to all nodes + info_repl_resps = await client.info([InfoSection.REPLICATION], AllNodes()) + Logger.log( + LogLevel.INFO, + "app", + f"INFO REPLICATION responses from all nodes are=\n{info_repl_resps!r}", + ) + + +async def exec_app_logic(): + """ + Executes the application logic with exception handling. + """ + while True: + try: + client = await create_client() + return await app_logic(client) + except asyncio.CancelledError: + raise + except ClosingError as e: + # If the error message contains "NOAUTH", raise the exception + # because it indicates a critical authentication issue. + if "NOAUTH" in str(e): + Logger.log( + LogLevel.ERROR, + "glide", + f"Authentication error encountered: {e}", + ) + raise e + Logger.log( + LogLevel.WARN, + "glide", + f"Client has closed and needs to be re-created: {e}", + ) + except TimeoutError as e: + # A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(LogLevel.ERROR, "glide", f"TimeoutError encountered: {e}") + raise e + except ConnectionError as e: + # The client wasn't able to reestablish the connection within the given retries + Logger.log(LogLevel.ERROR, "glide", f"ConnectionError encountered: {e}") + raise e + except RequestError as e: + # Other error reported during a request, such as a server response error + Logger.log(LogLevel.ERROR, "glide", f"RequestError encountered: {e}") + raise e + except Exception as e: + Logger.log(LogLevel.ERROR, "glide", f"Unexpected error: {e}") + raise e + finally: + try: + await client.close() + except Exception as e: + Logger.log( + LogLevel.WARN, + "glide", + f"Error encountered while closing the client: {e}", + ) + + +def main(): + # In this example, we will utilize the client's logger for all log messages + Logger.set_logger_config(LogLevel.INFO) + # Optional - set the logger to write to a file + # Logger.set_logger_config(LogLevel.INFO, file) + asyncio.run(exec_app_logic()) + + +if __name__ == "__main__": + main() diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt index 82e80dcc6b..cac8dc5db0 100644 --- a/examples/python/requirements.txt +++ b/examples/python/requirements.txt @@ -1 +1 @@ -glide-for-redis>=0.1 +valkey-glide diff --git a/examples/python/standalone_example.py b/examples/python/standalone_example.py new file mode 100644 index 0000000000..60e2b62dbb --- /dev/null +++ b/examples/python/standalone_example.py @@ -0,0 +1,129 @@ +import asyncio +from typing import List, Tuple + +from glide import ( + ClosingError, + ConnectionError, + GlideClient, + GlideClientConfiguration, + Logger, + LogLevel, + NodeAddress, + RequestError, + TimeoutError, +) + + +async def create_client( + nodes_list: List[Tuple[str, int]] = [("localhost", 6379)] +) -> GlideClient: + """ + Creates and returns a GlideClient instance. + + This function initializes a GlideClient with the provided list of nodes. + The nodes_list may contain either only primary node or a mix of primary + and replica nodes. The GlideClient use these nodes to connect to + the Standalone setup servers. + + Args: + nodes_list (List[Tuple[str, int]]): A list of tuples where each tuple + contains a host (str) and port (int). Defaults to [("localhost", 6379)]. + + Returns: + GlideClient: An instance of GlideClient connected to the specified nodes. + """ + addresses = [] + for host, port in nodes_list: + addresses.append(NodeAddress(host, port)) + + # Check `GlideClientConfiguration` for additional options. + config = GlideClientConfiguration( + addresses, + # Enable this field if the servers are configured with TLS. + # use_tls=True + ) + return await GlideClient.create(config) + + +async def app_logic(client: GlideClient): + """ + Executes the main logic of the application, performing basic operations + such as SET, GET, and PING using the provided GlideClient. + + Args: + client (GlideClient): An instance of GlideClient. + """ + # Send SET and GET + set_response = await client.set("foo", "bar") + Logger.log(LogLevel.INFO, "app", f"Set response is = {set_response!r}") + + get_response = await client.get("foo") + assert isinstance(get_response, bytes) + Logger.log(LogLevel.INFO, "app", f"Get response is = {get_response.decode()!r}") + + # Send PING to the primary node + pong = await client.ping() + Logger.log(LogLevel.INFO, "app", f"PING response is = {pong!r}") + + +async def exec_app_logic(): + """ + Executes the application logic with exception handling. + """ + while True: + try: + client = await create_client() + return await app_logic(client) + except asyncio.CancelledError: + raise + except ClosingError as e: + # If the error message contains "NOAUTH", raise the exception + # because it indicates a critical authentication issue. + if "NOAUTH" in str(e): + Logger.log( + LogLevel.ERROR, + "glide", + f"Authentication error encountered: {e}", + ) + raise e + Logger.log( + LogLevel.WARN, + "glide", + f"Client has closed and needs to be re-created: {e}", + ) + except TimeoutError as e: + # A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(LogLevel.ERROR, "glide", f"TimeoutError encountered: {e}") + raise e + except ConnectionError as e: + # The client wasn't able to reestablish the connection within the given retries + Logger.log(LogLevel.ERROR, "glide", f"ConnectionError encountered: {e}") + raise e + except RequestError as e: + # Other error reported during a request, such as a server response error + Logger.log(LogLevel.ERROR, "glide", f"RequestError encountered: {e}") + raise e + except Exception as e: + Logger.log(LogLevel.ERROR, "glide", f"Unexpected error: {e}") + raise e + finally: + try: + await client.close() + except Exception as e: + Logger.log( + LogLevel.WARN, + "glide", + f"Encountered an error while closing the client: {e}", + ) + + +def main(): + # In this example, we will utilize the client's logger for all log messages + Logger.set_logger_config(LogLevel.INFO) + # Optional - set the logger to write to a file + # Logger.set_logger_config(LogLevel.INFO, file) + asyncio.run(exec_app_logic()) + + +if __name__ == "__main__": + main() diff --git a/glide-core/Cargo.toml b/glide-core/Cargo.toml index 8e60ad0bb3..1d0b7d2967 100644 --- a/glide-core/Cargo.toml +++ b/glide-core/Cargo.toml @@ -3,12 +3,12 @@ name = "glide-core" version = "0.1.0" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bytes = { version = "^1.3", optional = true } +bytes = "1" futures = "^0.3" redis = { path = "../submodules/redis-rs/redis", features = ["aio", "tokio-comp", "tokio-rustls-comp", "connection-manager","cluster", "cluster-async"] } tokio = { version = "1", features = ["macros", "time"] } @@ -20,21 +20,23 @@ tokio-retry = "0.3.0" protobuf = { version= "3", features = ["bytes", "with-bytes"], optional = true } integer-encoding = { version = "4.0.0", optional = true } thiserror = "1" -rand = { version = "0.8.5", optional = true } +rand = { version = "0.8.5" } futures-intrusive = "0.5.0" directories = { version = "4.0", optional = true } once_cell = "1.18.0" arcstr = "1.1.5" sha1_smol = "1.0.0" +nanoid = "0.4.0" [features] -socket-layer = ["directories", "integer-encoding", "num_cpus", "protobuf", "tokio-util", "bytes", "rand"] +socket-layer = ["directories", "integer-encoding", "num_cpus", "protobuf", "tokio-util"] [dev-dependencies] rsevents = "0.3.1" socket2 = "^0.5" tempfile = "3.3.0" rstest = "^0.18" +serial_test = "3" criterion = { version = "^0.5", features = ["html_reports", "async_tokio"] } which = "5" ctor = "0.2.2" diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index 9bdfe689c2..0bc2e35712 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -212,7 +212,7 @@ The applicable license information is listed below: ---- -Package: addr2line:0.21.0 +Package: addr2line:0.22.0 The following copyrights and licenses were found in the source code of this package: @@ -1828,7 +1828,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: arcstr:1.1.5 +Package: arcstr:1.2.0 The following copyrights and licenses were found in the source code of this package: @@ -2077,7 +2077,7 @@ the following restrictions: ---- -Package: async-trait:0.1.80 +Package: async-trait:0.1.81 The following copyrights and licenses were found in the source code of this package: @@ -2535,7 +2535,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: backtrace:0.3.71 +Package: backtrace:0.3.73 The following copyrights and licenses were found in the source code of this package: @@ -2764,7 +2764,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: base64:0.22.0 +Package: base64:0.22.1 The following copyrights and licenses were found in the source code of this package: @@ -2993,7 +2993,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bitflags:1.3.2 +Package: bitflags:2.6.0 The following copyrights and licenses were found in the source code of this package: @@ -3222,7 +3222,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bitflags:2.5.0 +Package: bumpalo:3.16.0 The following copyrights and licenses were found in the source code of this package: @@ -3451,7 +3451,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bumpalo:3.16.0 +Package: bytes:1.6.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: cfg-if:1.0.0 The following copyrights and licenses were found in the source code of this package: @@ -3680,10 +3705,214 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bytes:1.6.0 +Package: chrono:0.4.38 The following copyrights and licenses were found in the source code of this package: + 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 [yyyy] [name of copyright owner] + + 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. + + -- + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -3705,7 +3934,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: cfg-if:1.0.0 +Package: combine:4.6.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: core-foundation:0.9.4 The following copyrights and licenses were found in the source code of this package: @@ -3934,7 +4188,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: chrono:0.4.38 +Package: core-foundation-sys:0.8.6 The following copyrights and licenses were found in the source code of this package: @@ -4163,7 +4417,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: combine:4.6.7 +Package: crc16:0.4.0 The following copyrights and licenses were found in the source code of this package: @@ -4188,7 +4442,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation:0.9.4 +Package: crc32fast:1.4.2 The following copyrights and licenses were found in the source code of this package: @@ -4417,7 +4671,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: crossbeam-channel:0.5.13 The following copyrights and licenses were found in the source code of this package: @@ -4646,32 +4900,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crc16:0.4.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: crc32fast:1.4.0 +Package: crossbeam-utils:0.8.20 The following copyrights and licenses were found in the source code of this package: @@ -4900,7 +5129,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-channel:0.5.12 +Package: deranged:0.3.11 The following copyrights and licenses were found in the source code of this package: @@ -5129,7 +5358,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-utils:0.8.19 +Package: derivative:2.2.0 The following copyrights and licenses were found in the source code of this package: @@ -5358,7 +5587,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: deranged:0.3.11 +Package: directories:4.0.1 The following copyrights and licenses were found in the source code of this package: @@ -5587,7 +5816,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: derivative:2.2.0 +Package: dirs-sys:0.3.7 The following copyrights and licenses were found in the source code of this package: @@ -5816,7 +6045,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: directories:4.0.1 +Package: dispose:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -6045,7 +6274,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dirs-sys:0.3.7 +Package: dispose-derive:0.4.0 The following copyrights and licenses were found in the source code of this package: @@ -6274,7 +6503,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dispose:0.5.0 +Package: fast-math:0.1.1 The following copyrights and licenses were found in the source code of this package: @@ -6503,7 +6732,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dispose-derive:0.4.0 +Package: file-rotate:0.7.6 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: flate2:1.0.30 The following copyrights and licenses were found in the source code of this package: @@ -6732,7 +6986,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: fast-math:0.1.1 +Package: form_urlencoded:1.2.1 The following copyrights and licenses were found in the source code of this package: @@ -6961,32 +7215,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: file-rotate:0.7.5 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: flate2:1.0.28 +Package: futures:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7215,7 +7444,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: form_urlencoded:1.2.1 +Package: futures-channel:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7444,7 +7673,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures:0.3.30 +Package: futures-core:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7673,7 +7902,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-channel:0.3.30 +Package: futures-executor:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7902,7 +8131,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-core:0.3.30 +Package: futures-intrusive:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -8131,7 +8360,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-executor:0.3.30 +Package: futures-io:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8360,7 +8589,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-intrusive:0.5.0 +Package: futures-macro:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8589,7 +8818,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-io:0.3.30 +Package: futures-sink:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8818,7 +9047,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-macro:0.3.30 +Package: futures-task:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9047,7 +9276,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-sink:0.3.30 +Package: futures-util:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9276,7 +9505,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-task:0.3.30 +Package: getrandom:0.2.15 The following copyrights and licenses were found in the source code of this package: @@ -9505,7 +9734,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-util:0.3.30 +Package: gimli:0.29.0 The following copyrights and licenses were found in the source code of this package: @@ -9734,7 +9963,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: getrandom:0.2.14 +Package: hashbrown:0.14.5 The following copyrights and licenses were found in the source code of this package: @@ -9963,7 +10192,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: gimli:0.28.1 +Package: heck:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -10192,7 +10421,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hashbrown:0.14.3 +Package: hermit-abi:0.3.9 The following copyrights and licenses were found in the source code of this package: @@ -10421,7 +10650,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.9 +Package: iana-time-zone:0.1.60 The following copyrights and licenses were found in the source code of this package: @@ -10650,7 +10879,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: iana-time-zone:0.1.60 +Package: iana-time-zone-haiku:0.1.2 The following copyrights and licenses were found in the source code of this package: @@ -10879,7 +11108,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: iana-time-zone-haiku:0.1.2 +Package: idna:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -11108,7 +11337,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: idna:0.5.0 +Package: ieee754:0.2.6 The following copyrights and licenses were found in the source code of this package: @@ -11337,7 +11566,63 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ieee754:0.2.6 +Package: instant:0.1.13 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: integer-encoding:4.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: itoa:1.0.11 The following copyrights and licenses were found in the source code of this package: @@ -11566,63 +11851,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: instant:0.1.12 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of the ORGANIZATION nor the names of its contributors may be -used to endorse or promote products derived from this software without specific -prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - -Package: integer-encoding:4.0.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: itoa:1.0.11 +Package: js-sys:0.3.69 The following copyrights and licenses were found in the source code of this package: @@ -11851,7 +12080,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.69 +Package: lazy_static:1.5.0 The following copyrights and licenses were found in the source code of this package: @@ -12080,7 +12309,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: lazy_static:1.4.0 +Package: libc:0.2.155 The following copyrights and licenses were found in the source code of this package: @@ -12309,7 +12538,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.153 +Package: libredox:0.1.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: lock_api:0.4.12 The following copyrights and licenses were found in the source code of this package: @@ -12538,32 +12792,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libredox:0.1.3 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: lock_api:0.4.11 +Package: log:0.4.22 The following copyrights and licenses were found in the source code of this package: @@ -12792,7 +13021,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: log:0.4.21 +Package: logger_core:0.1.0 The following copyrights and licenses were found in the source code of this package: @@ -12998,7 +13227,11 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. - -- +---- + +Package: memchr:2.7.4 + +The following copyrights and licenses were found in the source code of this package: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -13019,9 +13252,36 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + -- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + ---- -Package: logger_core:0.1.0 +Package: miniz_oxide:0.7.4 The following copyrights and licenses were found in the source code of this package: @@ -13227,9 +13487,50 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + ---- -Package: memchr:2.7.2 +Package: mio:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -13252,36 +13553,59 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -- +---- -This is free and unencumbered software released into the public domain. +Package: nanoid:0.4.0 -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. +The following copyrights and licenses were found in the source code of this package: -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to +---- + +Package: nu-ansi-term:0.46.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: miniz_oxide:0.7.2 +Package: num-bigint:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -13500,51 +13824,6 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -- - -This software is provided 'as-is', without any express or implied warranty. In no -event will the authors be held liable for any damages arising from the use of this -software. - -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that - you wrote the original software. If you use this software in a product, an - acknowledgment in the product documentation would be appreciated but is not - required. - -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - ----- - -Package: mio:0.8.11 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. @@ -13555,32 +13834,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: nu-ansi-term:0.46.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: num-bigint:0.4.4 +Package: num-conv:0.1.0 The following copyrights and licenses were found in the source code of this package: @@ -13809,7 +14063,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-conv:0.1.0 +Package: num-integer:0.1.46 The following copyrights and licenses were found in the source code of this package: @@ -14038,7 +14292,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-integer:0.1.46 +Package: num-traits:0.2.19 The following copyrights and licenses were found in the source code of this package: @@ -14267,7 +14521,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-traits:0.2.18 +Package: num_cpus:1.16.0 The following copyrights and licenses were found in the source code of this package: @@ -14496,7 +14750,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num_cpus:1.16.0 +Package: object:0.36.1 The following copyrights and licenses were found in the source code of this package: @@ -14725,7 +14979,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.32.2 +Package: once_cell:1.19.0 The following copyrights and licenses were found in the source code of this package: @@ -14954,7 +15208,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: once_cell:1.19.0 +Package: openssl-probe:0.1.5 The following copyrights and licenses were found in the source code of this package: @@ -15183,7 +15437,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: openssl-probe:0.1.5 +Package: overload:0.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: parking_lot:0.12.3 The following copyrights and licenses were found in the source code of this package: @@ -15412,32 +15691,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: overload:0.1.1 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: parking_lot:0.12.1 +Package: parking_lot_core:0.9.10 The following copyrights and licenses were found in the source code of this package: @@ -15666,7 +15920,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: parking_lot_core:0.9.9 +Package: percent-encoding:2.3.1 The following copyrights and licenses were found in the source code of this package: @@ -15895,7 +16149,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: percent-encoding:2.3.1 +Package: pin-project:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -16124,7 +16378,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project:1.1.5 +Package: pin-project-internal:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -16353,7 +16607,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project-internal:1.1.5 +Package: pin-project-lite:0.2.14 The following copyrights and licenses were found in the source code of this package: @@ -16582,7 +16836,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project-lite:0.2.14 +Package: pin-utils:0.1.0 The following copyrights and licenses were found in the source code of this package: @@ -16811,7 +17065,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-utils:0.1.0 +Package: powerfmt:0.2.0 The following copyrights and licenses were found in the source code of this package: @@ -17040,7 +17294,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: powerfmt:0.2.0 +Package: ppv-lite86:0.2.17 The following copyrights and licenses were found in the source code of this package: @@ -17269,7 +17523,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ppv-lite86:0.2.17 +Package: proc-macro-error:1.0.4 The following copyrights and licenses were found in the source code of this package: @@ -17498,7 +17752,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: proc-macro-error:1.0.4 +Package: proc-macro-error-attr:1.0.4 The following copyrights and licenses were found in the source code of this package: @@ -17727,7 +17981,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: proc-macro-error-attr:1.0.4 +Package: proc-macro2:1.0.86 The following copyrights and licenses were found in the source code of this package: @@ -17956,7 +18210,57 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: proc-macro2:1.0.81 +Package: protobuf:3.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: protobuf-support:3.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: quote:1.0.36 The following copyrights and licenses were found in the source code of this package: @@ -18185,57 +18489,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.4.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: protobuf-support:3.4.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: quote:1.0.36 +Package: rand:0.8.5 The following copyrights and licenses were found in the source code of this package: @@ -18464,7 +18718,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rand:0.8.5 +Package: rand_chacha:0.3.1 The following copyrights and licenses were found in the source code of this package: @@ -18693,7 +18947,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rand_chacha:0.3.1 +Package: rand_core:0.6.4 The following copyrights and licenses were found in the source code of this package: @@ -18922,7 +19176,88 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rand_core:0.6.4 +Package: redis:0.25.2 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: redox_syscall:0.5.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: redox_users:0.4.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustc-demangle:0.1.24 The following copyrights and licenses were found in the source code of this package: @@ -19151,88 +19486,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: redis:0.25.2 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of the ORGANIZATION nor the names of its contributors may be -used to endorse or promote products derived from this software without specific -prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - -Package: redox_syscall:0.4.1 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: redox_users:0.4.5 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: rustc-demangle:0.1.23 +Package: rustls:0.22.4 The following copyrights and licenses were found in the source code of this package: @@ -19440,6 +19694,20 @@ The following copyrights and licenses were found in the source code of this pack -- +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + -- + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -19461,7 +19729,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls:0.22.3 +Package: rustls-native-certs:0.7.1 The following copyrights and licenses were found in the source code of this package: @@ -19704,7 +19972,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.0 +Package: rustls-pemfile:2.1.2 The following copyrights and licenses were found in the source code of this package: @@ -19947,7 +20215,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.2 +Package: rustls-pki-types:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -20155,20 +20423,6 @@ The following copyrights and licenses were found in the source code of this pack -- -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. - - -- - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -20190,7 +20444,25 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.4.1 +Package: rustls-webpki:0.102.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +---- + +Package: rustversion:1.0.17 The following copyrights and licenses were found in the source code of this package: @@ -20419,25 +20691,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.2 - -The following copyrights and licenses were found in the source code of this package: - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. - ----- - -Package: ryu:1.0.17 +Package: ryu:1.0.18 The following copyrights and licenses were found in the source code of this package: @@ -20925,7 +21179,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: security-framework:2.10.0 +Package: security-framework:2.11.1 The following copyrights and licenses were found in the source code of this package: @@ -21154,7 +21408,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: security-framework-sys:2.10.0 +Package: security-framework-sys:2.11.1 The following copyrights and licenses were found in the source code of this package: @@ -21383,7 +21637,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.198 +Package: serde:1.0.204 The following copyrights and licenses were found in the source code of this package: @@ -21612,7 +21866,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.198 +Package: serde_derive:1.0.204 The following copyrights and licenses were found in the source code of this package: @@ -22151,7 +22405,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: socket2:0.5.6 +Package: socket2:0.5.7 The following copyrights and licenses were found in the source code of this package: @@ -22405,7 +22659,57 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: subtle:2.5.0 +Package: strum:0.26.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: strum_macros:0.26.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: subtle:2.6.1 The following copyrights and licenses were found in the source code of this package: @@ -22665,7 +22969,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.60 +Package: syn:2.0.71 The following copyrights and licenses were found in the source code of this package: @@ -22894,7 +23198,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror:1.0.58 +Package: thiserror:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -23123,7 +23427,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.58 +Package: thiserror-impl:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -24268,7 +24572,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tinyvec:1.6.0 +Package: tinyvec:1.8.0 The following copyrights and licenses were found in the source code of this package: @@ -24766,7 +25070,7 @@ the following restrictions: ---- -Package: tokio:1.37.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: @@ -24791,7 +25095,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.2.0 +Package: tokio-macros:2.3.0 The following copyrights and licenses were found in the source code of this package: @@ -25070,7 +25374,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-util:0.7.10 +Package: tokio-util:0.7.11 The following copyrights and licenses were found in the source code of this package: @@ -29462,7 +29766,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-targets:0.52.5 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -29920,7 +30224,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_gnullvm:0.52.5 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -30378,7 +30682,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.5 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -30836,7 +31140,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.5 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -31065,7 +31369,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.5 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -31523,7 +31827,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.5 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -31981,7 +32285,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.5 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32439,7 +32743,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.52.5 +Package: windows_x86_64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32897,7 +33201,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.52.5 +Package: windows_x86_64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33126,7 +33430,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.7.32 +Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -33378,7 +33682,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.7.32 +Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -33630,7 +33934,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zeroize:1.7.0 +Package: zeroize:1.8.1 The following copyrights and licenses were found in the source code of this package: diff --git a/glide-core/benches/connections_benchmark.rs b/glide-core/benches/connections_benchmark.rs index fc98933de8..f52a91d3ca 100644 --- a/glide-core/benches/connections_benchmark.rs +++ b/glide-core/benches/connections_benchmark.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use criterion::{criterion_group, criterion_main, Criterion}; use futures::future::join_all; @@ -83,7 +83,7 @@ fn get_connection_info(address: ConnectionAddr) -> redis::ConnectionInfo { fn multiplexer_benchmark(c: &mut Criterion, address: ConnectionAddr, group: &str) { benchmark(c, address, "multiplexer", group, |address, runtime| { let client = redis::Client::open(get_connection_info(address)).unwrap(); - runtime.block_on(async { client.get_multiplexed_tokio_connection().await.unwrap() }) + runtime.block_on(async { client.get_multiplexed_tokio_connection(None).await.unwrap() }) }); } @@ -120,7 +120,7 @@ fn cluster_connection_benchmark( builder = builder.read_from_replicas(); } let client = builder.build().unwrap(); - client.get_async_connection().await + client.get_async_connection(None).await }) .unwrap() }); diff --git a/glide-core/benches/memory_benchmark.rs b/glide-core/benches/memory_benchmark.rs index c6e307bae2..1948d9a2cd 100644 --- a/glide-core/benches/memory_benchmark.rs +++ b/glide-core/benches/memory_benchmark.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use glide_core::{ client::Client, @@ -26,7 +26,7 @@ where { let runtime = Builder::new_current_thread().enable_all().build().unwrap(); runtime.block_on(async { - let client = Client::new(create_connection_request().into()) + let client = Client::new(create_connection_request().into(), None) .await .unwrap(); f(client).await; diff --git a/glide-core/benches/rotating_buffer_benchmark.rs b/glide-core/benches/rotating_buffer_benchmark.rs index 70d60ec3aa..224d1b702b 100644 --- a/glide-core/benches/rotating_buffer_benchmark.rs +++ b/glide-core/benches/rotating_buffer_benchmark.rs @@ -1,13 +1,13 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use std::io::Write; use bytes::BufMut; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use glide_core::{ - redis_request::{command, redis_request}, - redis_request::{Command, RedisRequest, RequestType}, + command_request::{command, command_request}, + command_request::{Command, CommandRequest, RequestType}, rotating_buffer::RotatingBuffer, }; use integer_encoding::VarInt; @@ -102,12 +102,13 @@ fn benchmark_split_data( benchmark(c, test_group, benchmark_fn, "split_data", split_data()); } -fn generate_random_string(length: usize) -> String { - rand::thread_rng() +fn generate_random_string(length: usize) -> bytes::Bytes { + let s: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(length) .map(char::from) - .collect() + .collect(); + bytes::Bytes::from(s) } fn write_length(buffer: &mut Vec, length: u32) { @@ -117,7 +118,7 @@ fn write_length(buffer: &mut Vec, length: u32) { u32::encode_var(length, &mut buffer[new_len - required_space..]); } -fn message_buffer(request: RedisRequest) -> Vec { +fn message_buffer(request: CommandRequest) -> Vec { let message_length = request.compute_size() as usize; let mut buffer = Vec::with_capacity(message_length); write_length(&mut buffer, message_length as u32); @@ -163,29 +164,29 @@ fn split_data() -> Vec> { vec![vec, vec1, vec2] } -fn create_request(args: Vec, args_pointer: bool) -> RedisRequest { - let mut request = RedisRequest::new(); +fn create_request(args: Vec, args_pointer: bool) -> CommandRequest { + let mut request = CommandRequest::new(); request.callback_idx = 1; let mut command = Command::new(); command.request_type = RequestType::CustomCommand.into(); if args_pointer { command.args = Some(command::Args::ArgsVecPointer(Box::leak(Box::new(args)) - as *mut Vec + as *mut Vec as u64)); } else { let mut args_array = command::ArgsArray::new(); - args_array.args = args.into_iter().map(|str| str.into()).collect(); + args_array.args = args; command.args = Some(command::Args::ArgsArray(args_array)); } - request.command = Some(redis_request::Command::SingleCommand(command)); + request.command = Some(command_request::Command::SingleCommand(command)); request } -fn short_request() -> RedisRequest { +fn short_request() -> CommandRequest { create_request(vec!["GET".into(), "goo".into(), "bar".into()], false) } -fn medium_request() -> RedisRequest { +fn medium_request() -> CommandRequest { create_request( vec![ "GET".into(), @@ -196,7 +197,7 @@ fn medium_request() -> RedisRequest { ) } -fn long_request(args_pointer: bool) -> RedisRequest { +fn long_request(args_pointer: bool) -> CommandRequest { create_request( vec![ "GET".into(), @@ -214,7 +215,7 @@ macro_rules! run_bench { $test_name($c, "rotating_buffer", |test_data: &Vec>| { for data in test_data { $rotating_buffer.current_buffer().put(&data[..]); - $rotating_buffer.get_requests::().unwrap(); + $rotating_buffer.get_requests::().unwrap(); } $rotating_buffer.current_buffer().clear() }); diff --git a/glide-core/build.rs b/glide-core/build.rs index 9d41cd2491..cd838c9a53 100644 --- a/glide-core/build.rs +++ b/glide-core/build.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ #[cfg(feature = "socket-layer")] @@ -11,7 +11,7 @@ fn build_protobuf() { protobuf_codegen::Codegen::new() .cargo_out_dir("protobuf") .include("src") - .input("src/protobuf/redis_request.proto") + .input("src/protobuf/command_request.proto") .input("src/protobuf/response.proto") .input("src/protobuf/connection_request.proto") .customize(customization_options) diff --git a/glide-core/src/client/mod.rs b/glide-core/src/client/mod.rs index 645bef1118..249a409041 100644 --- a/glide-core/src/client/mod.rs +++ b/glide-core/src/client/mod.rs @@ -1,26 +1,26 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ mod types; +use crate::cluster_scan_container::insert_cluster_scan_cursor; use crate::scripts_container::get_script; use futures::FutureExt; use logger_core::log_info; use redis::aio::ConnectionLike; use redis::cluster_async::ClusterConnection; -use redis::cluster_routing::{RoutingInfo, SingleNodeRoutingInfo}; -use redis::RedisResult; -use redis::{Cmd, ErrorKind, Value}; +use redis::cluster_routing::{Routable, RoutingInfo, SingleNodeRoutingInfo}; +use redis::{Cmd, ErrorKind, ObjectType, PushInfo, RedisError, RedisResult, ScanStateRC, Value}; pub use standalone_client::StandaloneClient; use std::io; -use std::ops::Deref; use std::time::Duration; pub use types::*; -use self::value_conversion::{convert_to_expected_type, expected_type_for_cmd}; +use self::value_conversion::{convert_to_expected_type, expected_type_for_cmd, get_value_type}; mod reconnecting_connection; mod standalone_client; mod value_conversion; +use tokio::sync::mpsc; pub const HEARTBEAT_SLEEP_DURATION: Duration = Duration::from_secs(1); @@ -28,6 +28,7 @@ pub const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_millis(250); pub const DEFAULT_CONNECTION_ATTEMPT_TIMEOUT: Duration = Duration::from_millis(250); pub const DEFAULT_PERIODIC_CHECKS_INTERVAL: Duration = Duration::from_secs(60); pub const INTERNAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(250); +pub const FINISHED_SCAN_CURSOR: &str = "finished"; pub(super) fn get_port(address: &NodeAddress) -> u16 { const DEFAULT_PORT: u16 = 6379; @@ -44,6 +45,7 @@ pub(super) fn get_redis_connection_info( let protocol = connection_request.protocol.unwrap_or_default(); let db = connection_request.database_id; let client_name = connection_request.client_name.clone(); + let pubsub_subscriptions = connection_request.pubsub_subscriptions.clone(); match &connection_request.authentication_info { Some(info) => redis::RedisConnectionInfo { db, @@ -51,11 +53,13 @@ pub(super) fn get_redis_connection_info( password: info.password.clone(), protocol, client_name, + pubsub_subscriptions, }, None => redis::RedisConnectionInfo { db, protocol, client_name, + pubsub_subscriptions, ..Default::default() }, } @@ -95,13 +99,122 @@ pub struct Client { } async fn run_with_timeout( - timeout: Duration, + timeout: Option, future: impl futures::Future> + Send, ) -> redis::RedisResult { - tokio::time::timeout(timeout, future) - .await - .map_err(|_| io::Error::from(io::ErrorKind::TimedOut).into()) - .and_then(|res| res) + match timeout { + Some(duration) => tokio::time::timeout(duration, future) + .await + .map_err(|_| io::Error::from(io::ErrorKind::TimedOut).into()) + .and_then(|res| res), + None => future.await, + } +} + +/// Extension to the request timeout for blocking commands to ensure we won't return with timeout error before the server responded +const BLOCKING_CMD_TIMEOUT_EXTENSION: f64 = 0.5; // seconds + +enum TimeUnit { + Milliseconds = 1000, + Seconds = 1, +} + +/// Enumeration representing different request timeout options. +#[derive(Default, PartialEq, Debug)] +enum RequestTimeoutOption { + // Indicates no timeout should be set for the request. + NoTimeout, + // Indicates the request timeout should be based on the client's configured timeout. + #[default] + ClientConfig, + // Indicates the request timeout should be based on the timeout specified in the blocking command. + BlockingCommand(Duration), +} + +/// Helper function for parsing a timeout argument to f64. +/// Attempts to parse the argument found at `timeout_idx` from bytes into an f64. +fn parse_timeout_to_f64(cmd: &Cmd, timeout_idx: usize) -> RedisResult { + let create_err = |err_msg| { + RedisError::from(( + ErrorKind::ResponseError, + err_msg, + format!( + "Expected to find timeout value at index {:?} for command {:?}.", + timeout_idx, + std::str::from_utf8(&cmd.command().unwrap_or_default()), + ), + )) + }; + let timeout_bytes = cmd + .arg_idx(timeout_idx) + .ok_or(create_err("Couldn't find timeout index"))?; + let timeout_str = std::str::from_utf8(timeout_bytes) + .map_err(|_| create_err("Failed to parse the timeout argument to string"))?; + timeout_str + .parse::() + .map_err(|_| create_err("Failed to parse the timeout argument to f64")) +} + +/// Attempts to get the timeout duration from the command argument at `timeout_idx`. +/// If the argument can be parsed into a duration, it returns the duration in seconds with BlockingCmdTimeout. +/// If the timeout argument value is zero, NoTimeout will be returned. Otherwise, ClientConfigTimeout is returned. +fn get_timeout_from_cmd_arg( + cmd: &Cmd, + timeout_idx: usize, + time_unit: TimeUnit, +) -> RedisResult { + let timeout_secs = parse_timeout_to_f64(cmd, timeout_idx)? / ((time_unit as i32) as f64); + if timeout_secs < 0.0 { + // Timeout cannot be negative, return the client's configured request timeout + Err(RedisError::from(( + ErrorKind::ResponseError, + "Timeout cannot be negative", + format!("Received timeout = {:?}.", timeout_secs), + ))) + } else if timeout_secs == 0.0 { + // `0` means we should set no timeout + Ok(RequestTimeoutOption::NoTimeout) + } else { + // We limit the maximum timeout due to restrictions imposed by Redis and the Duration crate + if timeout_secs > u32::MAX as f64 { + Err(RedisError::from(( + ErrorKind::ResponseError, + "Timeout is out of range, max timeout is 2^32 - 1 (u32::MAX)", + format!("Received timeout = {:?}.", timeout_secs), + ))) + } else { + // Extend the request timeout to ensure we don't timeout before receiving a response from the server. + Ok(RequestTimeoutOption::BlockingCommand( + Duration::from_secs_f64( + (timeout_secs + BLOCKING_CMD_TIMEOUT_EXTENSION).min(u32::MAX as f64), + ), + )) + } + } +} + +fn get_request_timeout(cmd: &Cmd, default_timeout: Duration) -> RedisResult> { + let command = cmd.command().unwrap_or_default(); + let timeout = match command.as_slice() { + b"BLPOP" | b"BRPOP" | b"BLMOVE" | b"BZPOPMAX" | b"BZPOPMIN" | b"BRPOPLPUSH" => { + get_timeout_from_cmd_arg(cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds) + } + b"BLMPOP" | b"BZMPOP" => get_timeout_from_cmd_arg(cmd, 1, TimeUnit::Seconds), + b"XREAD" | b"XREADGROUP" => cmd + .position(b"BLOCK") + .map(|idx| get_timeout_from_cmd_arg(cmd, idx + 1, TimeUnit::Milliseconds)) + .unwrap_or(Ok(RequestTimeoutOption::ClientConfig)), + b"WAIT" => get_timeout_from_cmd_arg(cmd, 2, TimeUnit::Milliseconds), + _ => Ok(RequestTimeoutOption::ClientConfig), + }?; + + match timeout { + RequestTimeoutOption::NoTimeout => Ok(None), + RequestTimeoutOption::ClientConfig => Ok(Some(default_timeout)), + RequestTimeoutOption::BlockingCommand(blocking_cmd_duration) => { + Ok(Some(blocking_cmd_duration)) + } + } } impl Client { @@ -111,7 +224,13 @@ impl Client { routing: Option, ) -> redis::RedisFuture<'a, Value> { let expected_type = expected_type_for_cmd(cmd); - run_with_timeout(self.request_timeout, async move { + let request_timeout = match get_request_timeout(cmd, self.request_timeout) { + Ok(request_timeout) => request_timeout, + Err(err) => { + return async { Err(err) }.boxed(); + } + }; + run_with_timeout(request_timeout, async move { match self.internal_client { ClientWrapper::Standalone(ref mut client) => client.send_command(cmd).await, @@ -127,6 +246,59 @@ impl Client { .boxed() } + // Cluster scan is not passed to redis-rs as a regular command, so we need to handle it separately. + // We send the command to a specific function in the redis-rs cluster client, which internally handles the + // the complication of a command scan, and generate the command base on the logic in the redis-rs library. + // + // The function returns a tuple with the cursor and the keys found in the scan. + // The cursor is not a regular cursor, but an ARC to a struct that contains the cursor and the data needed + // to continue the scan called ScanState. + // In order to avoid passing Rust GC to clean the ScanState when the cursor (ref) is passed to the wrapper, + // which means that Rust layer is not aware of the cursor anymore, we need to keep the ScanState alive. + // We do that by storing the ScanState in a global container, and return a cursor-id of the cursor to the wrapper. + // + // The wrapper create an object contain the cursor-id with a drop function that will remove the cursor from the container. + // When the ref is removed from the hash-map, there's no more references to the ScanState, and the GC will clean it. + pub async fn cluster_scan<'a>( + &'a mut self, + scan_state_cursor: &'a ScanStateRC, + match_pattern: &'a Option>, + count: Option, + object_type: Option, + ) -> RedisResult { + match self.internal_client { + ClientWrapper::Standalone(_) => { + unreachable!("Cluster scan is not supported in standalone mode") + } + ClientWrapper::Cluster { ref mut client } => { + let (cursor, keys) = match match_pattern { + Some(pattern) => { + client + .cluster_scan_with_pattern( + scan_state_cursor.clone(), + pattern, + count, + object_type, + ) + .await? + } + None => { + client + .cluster_scan(scan_state_cursor.clone(), count, object_type) + .await? + } + }; + + let cluster_cursor_id = if cursor.is_finished() { + Value::BulkString(FINISHED_SCAN_CURSOR.into()) + } else { + Value::BulkString(insert_cluster_scan_cursor(cursor).into()) + }; + Ok(Value::Array(vec![cluster_cursor_id, Value::Array(keys)])) + } + } + } + fn get_transaction_values( pipeline: &redis::Pipeline, mut values: Vec, @@ -147,7 +319,7 @@ impl Client { return Err(( ErrorKind::ResponseError, "Received non-array response for transaction", - format!("Response: `{value:?}`"), + format!("(response was {:?})", get_value_type(&value)), ) .into()); } @@ -189,7 +361,7 @@ impl Client { ) -> redis::RedisFuture<'a, Value> { let command_count = pipeline.cmd_iter().count(); let offset = command_count + 1; - run_with_timeout(self.request_timeout, async move { + run_with_timeout(Some(self.request_timeout), async move { let values = match self.internal_client { ClientWrapper::Standalone(ref mut client) => { client.send_pipeline(pipeline, offset, 1).await @@ -208,11 +380,11 @@ impl Client { .boxed() } - pub async fn invoke_script<'a, T: Deref>( + pub async fn invoke_script<'a>( &'a mut self, hash: &'a str, - keys: Vec, - args: Vec, + keys: &Vec<&[u8]>, + args: &Vec<&[u8]>, routing: Option, ) -> redis::RedisResult { let eval = eval_cmd(hash, keys, args); @@ -224,7 +396,7 @@ impl Client { let Some(code) = get_script(hash) else { return Err(err); }; - let load = load_cmd(code.as_str()); + let load = load_cmd(&code); self.send_command(&load, None).await?; self.send_command(&eval, routing).await } else { @@ -233,20 +405,20 @@ impl Client { } } -fn load_cmd(code: &str) -> Cmd { +fn load_cmd(code: &[u8]) -> Cmd { let mut cmd = redis::cmd("SCRIPT"); cmd.arg("LOAD").arg(code); cmd } -fn eval_cmd>(hash: &str, keys: Vec, args: Vec) -> Cmd { +fn eval_cmd(hash: &str, keys: &Vec<&[u8]>, args: &Vec<&[u8]>) -> Cmd { let mut cmd = redis::cmd("EVALSHA"); cmd.arg(hash).arg(keys.len()); for key in keys { - cmd.arg(&*key); + cmd.arg(key); } for arg in args { - cmd.arg(&*arg); + cmd.arg(arg); } cmd } @@ -259,6 +431,7 @@ fn to_duration(time_in_millis: Option, default: Duration) -> Duration { async fn create_cluster_client( request: ConnectionRequest, + push_sender: Option>, ) -> RedisResult { // TODO - implement timeout for each connection attempt let tls_mode = request.tls_mode.unwrap_or_default(); @@ -296,8 +469,11 @@ async fn create_cluster_client( }; builder = builder.tls(tls); } + if let Some(pubsub_subscriptions) = redis_connection_info.pubsub_subscriptions { + builder = builder.pubsub_subscriptions(pubsub_subscriptions); + } let client = builder.build()?; - client.get_async_connection().await + client.get_async_connection(push_sender).await } #[derive(thiserror::Error)] @@ -406,13 +582,22 @@ fn sanitized_request_string(request: &ConnectionRequest) -> String { String::new() }; + let pubsub_subscriptions = request + .pubsub_subscriptions + .as_ref() + .map(|pubsub_subscriptions| format!("\nPubsub subscriptions: {pubsub_subscriptions:?}")) + .unwrap_or_default(); + format!( - "\nAddresses: {addresses}{tls_mode}{cluster_mode}{request_timeout}{rfr_strategy}{connection_retry_strategy}{database_id}{protocol}{client_name}{periodic_checks}", + "\nAddresses: {addresses}{tls_mode}{cluster_mode}{request_timeout}{rfr_strategy}{connection_retry_strategy}{database_id}{protocol}{client_name}{periodic_checks}{pubsub_subscriptions}", ) } impl Client { - pub async fn new(request: ConnectionRequest) -> Result { + pub async fn new( + request: ConnectionRequest, + push_sender: Option>, + ) -> Result { const DEFAULT_CLIENT_CREATION_TIMEOUT: Duration = Duration::from_secs(10); log_info( @@ -422,13 +607,13 @@ impl Client { let request_timeout = to_duration(request.request_timeout, DEFAULT_RESPONSE_TIMEOUT); tokio::time::timeout(DEFAULT_CLIENT_CREATION_TIMEOUT, async move { let internal_client = if request.cluster_mode_enabled { - let client = create_cluster_client(request) + let client = create_cluster_client(request, push_sender) .await .map_err(ConnectionError::Cluster)?; ClientWrapper::Cluster { client } } else { ClientWrapper::Standalone( - StandaloneClient::create_client(request) + StandaloneClient::create_client(request, push_sender) .await .map_err(ConnectionError::Standalone)?, ) @@ -472,3 +657,164 @@ impl GlideClientForTests for StandaloneClient { self.send_command(cmd).boxed() } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use redis::Cmd; + + use crate::client::{ + get_request_timeout, RequestTimeoutOption, TimeUnit, BLOCKING_CMD_TIMEOUT_EXTENSION, + }; + + use super::get_timeout_from_cmd_arg; + + #[test] + fn test_get_timeout_from_cmd_returns_correct_duration_int() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg("5"); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + RequestTimeoutOption::BlockingCommand(Duration::from_secs_f64( + 5.0 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_timeout_from_cmd_returns_correct_duration_float() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg(0.5); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + RequestTimeoutOption::BlockingCommand(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_timeout_from_cmd_returns_correct_duration_milliseconds() { + let mut cmd = Cmd::new(); + cmd.arg("XREAD").arg("BLOCK").arg("500").arg("key"); + let result = get_timeout_from_cmd_arg(&cmd, 2, TimeUnit::Milliseconds); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + RequestTimeoutOption::BlockingCommand(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_timeout_from_cmd_returns_err_when_timeout_isnt_passed() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg("key3"); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_err()); + let err = result.unwrap_err(); + println!("{:?}", err); + assert!(err.to_string().to_lowercase().contains("index"), "{err}"); + } + + #[test] + fn test_get_timeout_from_cmd_returns_err_when_timeout_is_larger_than_u32_max() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP") + .arg("key1") + .arg("key2") + .arg(u32::MAX as u64 + 1); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_err()); + let err = result.unwrap_err(); + println!("{:?}", err); + assert!(err.to_string().to_lowercase().contains("u32"), "{err}"); + } + + #[test] + fn test_get_timeout_from_cmd_returns_err_when_timeout_is_negative() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg(-1); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().to_lowercase().contains("negative"), "{err}"); + } + + #[test] + fn test_get_timeout_from_cmd_returns_no_timeout_when_zero_is_passed() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg(0); + let result = get_timeout_from_cmd_arg(&cmd, cmd.args_iter().len() - 1, TimeUnit::Seconds); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), RequestTimeoutOption::NoTimeout,); + } + + #[test] + fn test_get_request_timeout_with_blocking_command_returns_cmd_arg_timeout() { + let mut cmd = Cmd::new(); + cmd.arg("BLPOP").arg("key1").arg("key2").arg("500"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 500.0 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + + let mut cmd = Cmd::new(); + cmd.arg("XREADGROUP").arg("BLOCK").arg("500").arg("key"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + + let mut cmd = Cmd::new(); + cmd.arg("BLMPOP").arg("0.857").arg("key"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 0.857 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + + let mut cmd = Cmd::new(); + cmd.arg("WAIT").arg(1).arg("500"); + let result = get_request_timeout(&cmd, Duration::from_millis(500)); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + Some(Duration::from_secs_f64( + 0.5 + BLOCKING_CMD_TIMEOUT_EXTENSION + )) + ); + } + + #[test] + fn test_get_request_timeout_non_blocking_command_returns_default_timeout() { + let mut cmd = Cmd::new(); + cmd.arg("SET").arg("key").arg("value").arg("PX").arg("500"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Some(Duration::from_millis(100))); + + let mut cmd = Cmd::new(); + cmd.arg("XREADGROUP").arg("key"); + let result = get_request_timeout(&cmd, Duration::from_millis(100)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Some(Duration::from_millis(100))); + } +} diff --git a/glide-core/src/client/reconnecting_connection.rs b/glide-core/src/client/reconnecting_connection.rs index c039d347bd..c76da9cf42 100644 --- a/glide-core/src/client/reconnecting_connection.rs +++ b/glide-core/src/client/reconnecting_connection.rs @@ -1,16 +1,18 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use super::{NodeAddress, TlsMode}; use crate::retry_strategies::RetryStrategy; use futures_intrusive::sync::ManualResetEvent; use logger_core::{log_debug, log_trace, log_warn}; use redis::aio::MultiplexedConnection; -use redis::{RedisConnectionInfo, RedisError, RedisResult}; +use redis::{PushInfo, RedisConnectionInfo, RedisError, RedisResult}; +use std::fmt; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; +use tokio::sync::mpsc; use tokio::task; use tokio_retry::Retry; @@ -44,12 +46,22 @@ struct InnerReconnectingConnection { #[derive(Clone)] pub(super) struct ReconnectingConnection { inner: Arc, + push_sender: Option>, } -async fn get_multiplexed_connection(client: &redis::Client) -> RedisResult { +impl fmt::Debug for ReconnectingConnection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.node_address()) + } +} + +async fn get_multiplexed_connection( + client: &redis::Client, + push_sender: Option>, +) -> RedisResult { run_with_timeout( - DEFAULT_CONNECTION_ATTEMPT_TIMEOUT, - client.get_multiplexed_async_connection(), + Some(DEFAULT_CONNECTION_ATTEMPT_TIMEOUT), + client.get_multiplexed_async_connection(push_sender), ) .await } @@ -57,9 +69,10 @@ async fn get_multiplexed_connection(client: &redis::Client) -> RedisResult>, ) -> Result { let client = &connection_backend.connection_info; - let action = || get_multiplexed_connection(client); + let action = || get_multiplexed_connection(client, push_sender.clone()); match Retry::spawn(retry_strategy.get_iterator(), action).await { Ok(connection) => { @@ -78,6 +91,7 @@ async fn create_connection( state: Mutex::new(ConnectionState::Connected(connection)), backend: connection_backend, }), + push_sender, }) } Err(err) => { @@ -96,6 +110,7 @@ async fn create_connection( state: Mutex::new(ConnectionState::InitializedDisconnected), backend: connection_backend, }), + push_sender, }; connection.reconnect(); Err((connection, err)) @@ -134,6 +149,7 @@ impl ReconnectingConnection { connection_retry_strategy: RetryStrategy, redis_connection_info: RedisConnectionInfo, tls_mode: TlsMode, + push_sender: Option>, ) -> Result { log_debug( "connection creation", @@ -146,7 +162,16 @@ impl ReconnectingConnection { connection_available_signal: ManualResetEvent::new(true), client_dropped_flagged: AtomicBool::new(false), }; - create_connection(backend, connection_retry_strategy).await + create_connection(backend, connection_retry_strategy, push_sender).await + } + + fn node_address(&self) -> String { + self.inner + .backend + .connection_info + .get_connection_info() + .addr + .to_string() } pub(super) fn is_dropped(&self) -> bool { @@ -195,6 +220,7 @@ impl ReconnectingConnection { log_debug("reconnect", "starting"); let connection_clone = self.clone(); + let push_sender = self.push_sender.clone(); // The reconnect task is spawned instead of awaited here, so that the reconnect attempt will continue in the // background, regardless of whether the calling task is dropped or not. task::spawn(async move { @@ -208,7 +234,7 @@ impl ReconnectingConnection { // Client was dropped, reconnection attempts can stop return; } - match get_multiplexed_connection(client).await { + match get_multiplexed_connection(client, push_sender.clone()).await { Ok(mut connection) => { if connection .send_packed_command(&redis::cmd("PING")) diff --git a/glide-core/src/client/standalone_client.rs b/glide-core/src/client/standalone_client.rs index 79246a7b76..727ad906d9 100644 --- a/glide-core/src/client/standalone_client.rs +++ b/glide-core/src/client/standalone_client.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use super::get_redis_connection_info; use super::reconnecting_connection::ReconnectingConnection; @@ -9,13 +9,16 @@ use futures::{future, stream, StreamExt}; #[cfg(standalone_heartbeat)] use logger_core::log_debug; use logger_core::log_warn; +use rand::Rng; use redis::cluster_routing::{self, is_readonly_cmd, ResponsePolicy, Routable, RoutingInfo}; -use redis::{RedisError, RedisResult, Value}; +use redis::{PushInfo, RedisError, RedisResult, Value}; use std::sync::atomic::AtomicUsize; use std::sync::Arc; +use tokio::sync::mpsc; #[cfg(standalone_heartbeat)] use tokio::task; +#[derive(Debug)] enum ReadFrom { Primary, PreferReplica { @@ -23,6 +26,7 @@ enum ReadFrom { }, } +#[derive(Debug)] struct DropWrapper { /// Connection to the primary node in the client. primary_index: usize, @@ -38,7 +42,7 @@ impl Drop for DropWrapper { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct StandaloneClient { inner: Arc, } @@ -46,6 +50,7 @@ pub struct StandaloneClient { pub enum StandaloneClientConnectionError { NoAddressesProvided, FailedConnection(Vec<(Option, RedisError)>), + PrimaryConflictFound(String), } impl std::fmt::Debug for StandaloneClientConnectionError { @@ -80,6 +85,12 @@ impl std::fmt::Debug for StandaloneClientConnectionError { }; Ok(()) } + StandaloneClientConnectionError::PrimaryConflictFound(found_primaries) => { + writeln!( + f, + "Primary conflict. More than one primary found in a Standalone setup: {found_primaries}" + ) + } } } } @@ -87,22 +98,33 @@ impl std::fmt::Debug for StandaloneClientConnectionError { impl StandaloneClient { pub async fn create_client( connection_request: ConnectionRequest, + push_sender: Option>, ) -> Result { if connection_request.addresses.is_empty() { return Err(StandaloneClientConnectionError::NoAddressesProvided); } - let redis_connection_info = get_redis_connection_info(&connection_request); + let mut redis_connection_info = get_redis_connection_info(&connection_request); + let pubsub_connection_info = redis_connection_info.clone(); + redis_connection_info.pubsub_subscriptions = None; let retry_strategy = RetryStrategy::new(connection_request.connection_retry_strategy); let tls_mode = connection_request.tls_mode; let node_count = connection_request.addresses.len(); + // randomize pubsub nodes, maybe a batter option is to always use the primary + let pubsub_node_index = rand::thread_rng().gen_range(0..node_count); + let pubsub_addr = &connection_request.addresses[pubsub_node_index]; let mut stream = stream::iter(connection_request.addresses.iter()) .map(|address| async { get_connection_and_replication_info( address, &retry_strategy, - &redis_connection_info, + if address.to_string() != pubsub_addr.to_string() { + &redis_connection_info + } else { + &pubsub_connection_info + }, tls_mode.unwrap_or(TlsMode::NoTls), + &push_sender, ) .await .map_err(|err| (format!("{}:{}", address.host, address.port), err)) @@ -116,11 +138,20 @@ impl StandaloneClient { match result { Ok((connection, replication_status)) => { nodes.push(connection); - if primary_index.is_none() - && redis::from_owned_redis_value::(replication_status) - .is_ok_and(|val| val.contains("role:master")) + if redis::from_owned_redis_value::(replication_status) + .is_ok_and(|val| val.contains("role:master")) { - primary_index = Some(nodes.len() - 1); + if let Some(primary_index) = primary_index { + // More than one primary found + return Err(StandaloneClientConnectionError::PrimaryConflictFound( + format!( + "Primary nodes: {:?}, {:?}", + nodes.pop(), + nodes.get(primary_index) + ), + )); + } + primary_index = Some(nodes.len().saturating_sub(1)); } } Err((address, (connection, err))) => { @@ -254,7 +285,7 @@ impl StandaloneClient { Some(ResponsePolicy::OneSucceeded) => future::select_ok(requests.map(Box::pin)) .await .map(|(result, _)| result), - Some(ResponsePolicy::OneSucceededNonEmpty) => { + Some(ResponsePolicy::FirstSucceededNonEmptyOrAllEmpty) => { future::select_ok(requests.map(|request| { Box::pin(async move { let result = request.await?; @@ -374,12 +405,14 @@ async fn get_connection_and_replication_info( retry_strategy: &RetryStrategy, connection_info: &redis::RedisConnectionInfo, tls_mode: TlsMode, + push_sender: &Option>, ) -> Result<(ReconnectingConnection, Value), (ReconnectingConnection, RedisError)> { let result = ReconnectingConnection::new( address, retry_strategy.clone(), connection_info.clone(), tls_mode, + push_sender.clone(), ) .await; let reconnecting_connection = match result { diff --git a/glide-core/src/client/types.rs b/glide-core/src/client/types.rs index f942f64174..c26cdfb93f 100644 --- a/glide-core/src/client/types.rs +++ b/glide-core/src/client/types.rs @@ -1,7 +1,9 @@ /* - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +use logger_core::log_warn; +use std::collections::HashSet; use std::time::Duration; #[cfg(feature = "socket-layer")] @@ -20,6 +22,7 @@ pub struct ConnectionRequest { pub request_timeout: Option, pub connection_retry_strategy: Option, pub periodic_checks: Option, + pub pubsub_subscriptions: Option, } pub struct AuthenticationInfo { @@ -150,6 +153,39 @@ impl From for ConnectionRequest { PeriodicCheck::Disabled } }); + let mut pubsub_subscriptions: Option = None; + if let Some(protobuf_pubsub) = value.pubsub_subscriptions.0 { + let mut redis_pubsub = redis::PubSubSubscriptionInfo::new(); + for (pubsub_type, channels_patterns) in + protobuf_pubsub.channels_or_patterns_by_type.iter() + { + let kind = match *pubsub_type { + 0 => redis::PubSubSubscriptionKind::Exact, + 1 => redis::PubSubSubscriptionKind::Pattern, + 2 => redis::PubSubSubscriptionKind::Sharded, + 3_u32..=u32::MAX => { + log_warn( + "client creation", + format!( + "Omitting pubsub subscription on an unknown type: {:?}", + *pubsub_type + ), + ); + continue; + } + }; + + for channel_pattern in channels_patterns.channels_or_patterns.iter() { + redis_pubsub + .entry(kind) + .and_modify(|channels_patterns| { + channels_patterns.insert(channel_pattern.to_vec()); + }) + .or_insert(HashSet::from([channel_pattern.to_vec()])); + } + } + pubsub_subscriptions = Some(redis_pubsub); + } ConnectionRequest { read_from, @@ -163,6 +199,7 @@ impl From for ConnectionRequest { request_timeout, connection_retry_strategy, periodic_checks, + pubsub_subscriptions, } } } diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 71d97b3d53..7a9ceced96 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -1,22 +1,41 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use redis::{ cluster_routing::Routable, from_owned_redis_value, Cmd, ErrorKind, RedisResult, Value, }; #[derive(Clone, Copy)] -pub(crate) enum ExpectedReturnType { - Map, +pub(crate) enum ExpectedReturnType<'a> { + Map { + key_type: &'a Option>, + value_type: &'a Option>, + }, MapOfStringToDouble, Double, Boolean, BulkString, Set, DoubleOrNull, - ZrankReturnType, + ZRankReturnType, JsonToggleReturnType, + ArrayOfStrings, ArrayOfBools, + ArrayOfDoubleOrNull, + Lolwut, + ArrayOfStringAndArrays, + ArrayOfArraysOfDoubleOrNull, + ArrayOfMaps(&'a Option>), + StringOrSet, + ArrayOfPairs, + ArrayOfMemberScorePairs, + ZMPopReturnType, + KeyWithMemberAndScore, + FunctionStatsReturnType, + GeoSearchReturnType, + SimpleString, + XAutoClaimReturnType, + XInfoStreamFullReturnType, } pub(crate) fn convert_to_expected_type( @@ -28,21 +47,23 @@ pub(crate) fn convert_to_expected_type( }; match expected { - ExpectedReturnType::Map => match value { + ExpectedReturnType::Map { + key_type, + value_type, + } => match value { Value::Nil => Ok(value), - Value::Map(_) => Ok(value), - Value::Array(array) => convert_array_to_map(array, None, None), + Value::Map(map) => convert_inner_map_by_type(map, *key_type, *value_type), + Value::Array(array) => convert_array_to_map_by_type(array, *key_type, *value_type), _ => Err(( ErrorKind::TypeError, "Response couldn't be converted to map", - format!("(response was {:?})", value), + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, ExpectedReturnType::MapOfStringToDouble => match value { Value::Nil => Ok(value), Value::Map(map) => { - let map_clone = map.clone(); let result = map .into_iter() .map(|(key, inner_value)| { @@ -59,7 +80,7 @@ pub(crate) fn convert_to_expected_type( _ => Err(( ErrorKind::TypeError, "Response couldn't be converted to map of {string: double}", - format!("(response was {:?})", map_clone), + format!("(response was {:?})", get_value_type(&inner_value)), ) .into()), } @@ -68,7 +89,7 @@ pub(crate) fn convert_to_expected_type( result.map(Value::Map) } - Value::Array(array) => convert_array_to_map( + Value::Array(array) => convert_array_to_map_by_type( array, Some(ExpectedReturnType::BulkString), Some(ExpectedReturnType::Double), @@ -76,7 +97,7 @@ pub(crate) fn convert_to_expected_type( _ => Err(( ErrorKind::TypeError, "Response couldn't be converted to map of {string: double}", - format!("(response was {:?})", value), + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, @@ -87,7 +108,7 @@ pub(crate) fn convert_to_expected_type( _ => Err(( ErrorKind::TypeError, "Response couldn't be converted to set", - format!("(response was {:?})", value), + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, @@ -97,7 +118,7 @@ pub(crate) fn convert_to_expected_type( Value::Nil => Ok(value), _ => Ok(Value::Double(from_owned_redis_value::(value)?)), }, - ExpectedReturnType::ZrankReturnType => match value { + ExpectedReturnType::ZRankReturnType => match value { Value::Nil => Ok(value), Value::Array(mut array) => { if array.len() != 2 { @@ -115,14 +136,17 @@ pub(crate) fn convert_to_expected_type( } _ => Err(( ErrorKind::TypeError, - "Response couldn't be converted to Array (ZrankResponseType)", - format!("(response was {:?})", value), + "Response couldn't be converted to Array (ZRankResponseType)", + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, ExpectedReturnType::BulkString => Ok(Value::BulkString( from_owned_redis_value::(value)?.into(), )), + ExpectedReturnType::SimpleString => Ok(Value::SimpleString( + from_owned_redis_value::(value)?, + )), ExpectedReturnType::JsonToggleReturnType => match value { Value::Array(array) => { let converted_array: RedisResult> = array @@ -134,7 +158,7 @@ pub(crate) fn convert_to_expected_type( _ => Err(( ErrorKind::TypeError, "Could not convert value to boolean", - format!("(value was {:?})", item), + format!("(response was {:?})", get_value_type(&item)), ) .into()), }, @@ -143,45 +167,874 @@ pub(crate) fn convert_to_expected_type( converted_array.map(Value::Array) } - Value::BulkString(bytes) => match std::str::from_utf8(&bytes) { + Value::BulkString(ref bytes) => match std::str::from_utf8(bytes) { Ok("true") => Ok(Value::Boolean(true)), Ok("false") => Ok(Value::Boolean(false)), _ => Err(( ErrorKind::TypeError, "Response couldn't be converted to boolean", - format!("(response was {:?})", bytes), + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, _ => Err(( ErrorKind::TypeError, "Response couldn't be converted to Json Toggle return type", - format!("(response was {:?})", value), + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, ExpectedReturnType::ArrayOfBools => match value { + Value::Array(array) => convert_array_elements(array, ExpectedReturnType::Boolean), + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of boolean", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + ExpectedReturnType::ArrayOfStrings => match value { + Value::Array(array) => convert_array_elements(array, ExpectedReturnType::BulkString), + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of bulk strings", + ) + .into()), + }, + ExpectedReturnType::ArrayOfDoubleOrNull => match value { + Value::Array(array) => convert_array_elements(array, ExpectedReturnType::DoubleOrNull), + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of doubles", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + // command returns nil or an array of 2 elements, where the second element is a map represented by a 2D array + // we convert that second element to a map as we do in `MapOfStringToDouble` + /* + > zmpop 1 z1 min count 10 + 1) "z1" + 2) 1) 1) "2" + 2) (double) 2 + 2) 1) "3" + 2) (double) 3 + */ + ExpectedReturnType::ZMPopReturnType => match value { + Value::Nil => Ok(value), + Value::Array(array) if array.len() == 2 && matches!(array[1], Value::Array(_)) => { + let Value::Array(nested_array) = array[1].clone() else { + unreachable!("Pattern match above ensures that it is Array") + }; + // convert the nested array to a map + let map = convert_array_to_map_by_type( + nested_array, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::Double), + )?; + Ok(Value::Array(vec![array[0].clone(), map])) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to ZMPOP return type", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + ExpectedReturnType::ArrayOfArraysOfDoubleOrNull => match value { + // This is used for GEOPOS command. Value::Array(array) => { - let array_of_bools = array - .iter() - .map(|v| Value::Boolean(from_owned_redis_value::(v.clone()).unwrap())) + let converted_array: RedisResult> = array + .clone() + .into_iter() + .map(|item| match item { + Value::Nil => Ok(Value::Nil), + Value::Array(mut inner_array) => { + if inner_array.len() != 2 { + return Err(( + ErrorKind::TypeError, + "Inner Array must contain exactly two elements", + ) + .into()); + } + inner_array[0] = convert_to_expected_type( + inner_array[0].clone(), + Some(ExpectedReturnType::Double), + )?; + inner_array[1] = convert_to_expected_type( + inner_array[1].clone(), + Some(ExpectedReturnType::Double), + )?; + + Ok(Value::Array(inner_array)) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of array of double or null. Inner value of Array must be Array or Null", + format!("(response was {:?})", get_value_type(&item)), + ) + .into()), + }) .collect(); - Ok(Value::Array(array_of_bools)) + + converted_array.map(Value::Array) } _ => Err(( ErrorKind::TypeError, - "Response couldn't be converted to an array of boolean", - format!("(response was {:?})", value), + "Response couldn't be converted to an array of array of double or null", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + ExpectedReturnType::Lolwut => { + match value { + // cluster (multi-node) response - go recursive + Value::Map(map) => convert_map_entries( + map, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::Lolwut), + ), + // RESP 2 response + Value::BulkString(bytes) => { + let text = std::str::from_utf8(&bytes).unwrap(); + let res = convert_lolwut_string(text); + Ok(Value::BulkString(Vec::from(res))) + } + // RESP 3 response + Value::VerbatimString { + format: _, + ref text, + } => { + let res = convert_lolwut_string(text); + Ok(Value::BulkString(Vec::from(res))) + } + _ => Err(( + ErrorKind::TypeError, + "LOLWUT response couldn't be converted to a user-friendly format", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + } + } + // Used by HRANDFIELD when the WITHVALUES arg is passed. + // The server response can be an empty array, a flat array of key-value pairs, or a two-dimensional array of key-value pairs. + // The conversions we do here are as follows: + // + // - if the server returned an empty array, return an empty array + // - if the server returned a flat array of key-value pairs, convert to a two-dimensional array of key-value pairs + // - if the server returned a two-dimensional array of key-value pairs, return as-is + ExpectedReturnType::ArrayOfPairs => convert_to_array_of_pairs(value, None), + // Used by ZRANDMEMBER when the WITHSCORES arg is passed. + // The server response can be an empty array, a flat array of member-score pairs, or a two-dimensional array of member-score pairs. + // The server response scores can be strings or doubles. The conversions we do here are as follows: + // + // - if the server returned an empty array, return an empty array + // - if the server returned a flat array of member-score pairs, convert to a two-dimensional array of member-score pairs. The scores are converted from type string to type double. + // - if the server returned a two-dimensional array of key-value pairs, return as-is. The scores will already be of type double since this is a RESP3 response. + ExpectedReturnType::ArrayOfMemberScorePairs => { + // RESP2 returns scores as strings, but we want scores as type double. + convert_to_array_of_pairs(value, Some(ExpectedReturnType::Double)) + } + // Used by LMPOP and BLMPOP + // The server response can be an array or null + // + // Example: + // let input = ["key", "val1", "val2"] + // let expected =("key", vec!["val1", "val2"]) + ExpectedReturnType::ArrayOfStringAndArrays => match value { + Value::Nil => Ok(value), + Value::Array(array) if array.len() == 2 && matches!(array[1], Value::Array(_)) => { + // convert the array to a map of string to string-array + let map = convert_array_to_map_by_type( + array, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::ArrayOfStrings), + )?; + Ok(map) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to a pair of String/String-Array return type", + ) + .into()), + }, + // Used by BZPOPMIN/BZPOPMAX, which return an array consisting of the key of the sorted set that was popped, the popped member, and its score. + // RESP2 returns the score as a string, but RESP3 returns the score as a double. Here we convert string scores into type double. + ExpectedReturnType::KeyWithMemberAndScore => match value { + Value::Nil => Ok(value), + Value::Array(ref array) if array.len() == 3 && matches!(array[2], Value::Double(_)) => { + Ok(value) + } + Value::Array(mut array) + if array.len() == 3 + && matches!(array[2], Value::BulkString(_) | Value::SimpleString(_)) => + { + array[2] = + convert_to_expected_type(array[2].clone(), Some(ExpectedReturnType::Double))?; + Ok(Value::Array(array)) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array containing a key, member, and score", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + // Used by GEOSEARCH. + // When all options are specified (withcoord, withdist, withhash) , the response looks like this: [[name (str), [dist (str), hash (int), [lon (str), lat (str)]]]] for RESP2. + // RESP3 return type is: [[name (str), [dist (str), hash (int), [lon (float), lat (float)]]]]. + // We also want to convert dist into float. + /* from this: + > GEOSEARCH Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST WITHHASH + 1) 1) "Catania" + 2) "56.4413" + 3) (integer) 3479447370796909 + 4) 1) "15.08726745843887329" + 2) "37.50266842333162032" + to this: + > GEOSEARCH Sicily FROMLONLAT 15 37 BYBOX 400 400 km ASC WITHCOORD WITHDIST WITHHASH + 1) 1) "Catania" + 2) (double) 56.4413 + 3) (integer) 3479447370796909 + 4) 1) (double) 15.08726745843887329 + 2) (double) 37.50266842333162032 + */ + ExpectedReturnType::GeoSearchReturnType => match value { + Value::Array(array) => { + let mut converted_array = Vec::with_capacity(array.len()); + for item in &array { + if let Value::Array(inner_array) = item { + if let Some((name, rest)) = inner_array.split_first() { + let rest = rest.iter().map(|v| { + match v { + Value::Array(coord) => { + // This is the [lon (str), lat (str)] that should be converted into [lon (float), lat (float)]. + if coord.len() != 2 { + Err(( + ErrorKind::TypeError, + "Inner Array must contain exactly two elements, longitude and latitude", + ).into()) + } else { + coord.iter() + .map(|elem| convert_to_expected_type(elem.clone(), Some(ExpectedReturnType::Double))) + .collect::, _>>() + .map(Value::Array) + } + } + Value::BulkString(dist) => { + // This is the conversion of dist from string to float + convert_to_expected_type( + Value::BulkString(dist.clone()), + Some(ExpectedReturnType::Double), + ) + } + _ => Ok(v.clone()), // Hash is both integer for RESP2/3 + } + }).collect::, _>>()?; + + converted_array + .push(Value::Array(vec![name.clone(), Value::Array(rest)])); + } else { + return Err(( + ErrorKind::TypeError, + "Response couldn't be converted to GeoSeatch return type, Inner Array must contain at least one element", + ) + .into()); + } + } else { + return Err(( + ErrorKind::TypeError, + "Response couldn't be converted to GeoSeatch return type, Expected an array as an inner element", + ) + .into()); + } + } + Ok(Value::Array(converted_array)) + } + + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to GeoSeatch return type, Expected an array as the outer elemen.", + ) + .into()), + }, + // `FUNCTION LIST` returns an array of maps with nested list of maps. + // In RESP2 these maps are represented by arrays - we're going to convert them. + /* RESP2 response + 1) 1) "library_name" + 2) "mylib1" + 3) "engine" + 4) "LUA" + 5) "functions" + 6) 1) 1) "name" + 2) "myfunc1" + 3) "description" + 4) (nil) + 5) "flags" + 6) (empty array) + 2) 1) "name" + ... + 2) 1) "library_name" + ... + + RESP3 response + 1) 1# "library_name" => "mylib1" + 2# "engine" => "LUA" + 3# "functions" => + 1) 1# "name" => "myfunc1" + 2# "description" => (nil) + 3# "flags" => (empty set) + 2) 1# "name" => "myfunc2" + ... + 2) 1# "library_name" => "mylib2" + ... + */ + ExpectedReturnType::ArrayOfMaps(type_of_map_values) => match value { + // empty array, or it is already contains a map (RESP3 response) - no conversion needed + Value::Array(ref array) if array.is_empty() || matches!(array[0], Value::Map(_)) => { + Ok(value) + } + Value::Array(array) => convert_array_of_flat_maps(array, *type_of_map_values), + // cluster (multi-node) response - go recursive + Value::Map(map) => convert_map_entries( + map, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::ArrayOfMaps(type_of_map_values)), + ), + // Due to recursion, this will convert every map value, including simple strings, which we do nothing with + Value::BulkString(_) | Value::SimpleString(_) | Value::VerbatimString { .. } => { + Ok(value) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + // Not used for a command, but used as a helper for `FUNCTION LIST` to process the inner map. + // It may contain a string (name, description) or set (flags), or nil (description). + // The set is stored as array in RESP2. See example for `ArrayOfMaps` above. + ExpectedReturnType::StringOrSet => match value { + Value::Array(_) => convert_to_expected_type(value, Some(ExpectedReturnType::Set)), + Value::Nil + | Value::BulkString(_) + | Value::SimpleString(_) + | Value::VerbatimString { .. } => Ok(value), + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted", + format!("(response was {:?})", get_value_type(&value)), ) .into()), }, + // `FUNCTION STATS` returns nested maps with different types of data + /* RESP2 response example + 1) "running_script" + 2) 1) "name" + 2) "" + 3) "command" + 4) 1) "fcall" + 2) "" + ... rest `fcall` args ... + 5) "duration_ms" + 6) (integer) 24529 + 3) "engines" + 4) 1) "LUA" + 2) 1) "libraries_count" + 2) (integer) 3 + 3) "functions_count" + 4) (integer) 5 + + 1) "running_script" + 2) (nil) + 3) "engines" + 4) ... + + RESP3 response example + 1# "running_script" => + 1# "name" => "" + 2# "command" => + 1) "fcall" + 2) "" + ... rest `fcall` args ... + 3# "duration_ms" => (integer) 5000 + 2# "engines" => + 1# "LUA" => + 1# "libraries_count" => (integer) 3 + 2# "functions_count" => (integer) 5 + */ + // First part of the response (`running_script`) is converted as `Map[str, any]` + // Second part is converted as `Map[str, Map[str, int]]` + ExpectedReturnType::FunctionStatsReturnType => match value { + // TODO reuse https://github.com/Bit-Quill/glide-for-redis/pull/331 and https://github.com/valkey-io/valkey-glide/pull/1489 + Value::Map(map) => { + if map[0].0 == Value::BulkString(b"running_script".into()) { + // already a RESP3 response - do nothing + Ok(Value::Map(map)) + } else { + // cluster (multi-node) response - go recursive + convert_map_entries( + map, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::FunctionStatsReturnType), + ) + } + } + Value::Array(mut array) if array.len() == 4 => { + let mut result: Vec<(Value, Value)> = Vec::with_capacity(2); + let running_script_info = array.remove(1); + let running_script_converted = match running_script_info { + Value::Nil => Ok(Value::Nil), + Value::Array(inner_map_as_array) => { + convert_array_to_map_by_type(inner_map_as_array, None, None) + } + _ => Err((ErrorKind::TypeError, "Response couldn't be converted").into()), + }; + result.push((array.remove(0), running_script_converted?)); + let Value::Array(engines_info) = array.remove(1) else { + return Err((ErrorKind::TypeError, "Incorrect value type received").into()); + }; + let engines_info_converted = convert_array_to_map_by_type( + engines_info, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }), + ); + result.push((array.remove(0), engines_info_converted?)); + + Ok(Value::Map(result)) + } + _ => Err((ErrorKind::TypeError, "Response couldn't be converted").into()), + }, + // Used by XAUTOCLAIM. The command returns a list of length 2 if the server version is less than 7.0.0 or a list + // of length 3 otherwise. It has the following response format: + /* server version < 7.0.0 example: + 1) "0-0" + 2) 1) 1) "1-0" + 2) 1) "field1" + 2) "value1" + 3) "field2" + 4) "value2" + 2) 1) "1-1" + 2) 1) "field3" + 2) "value3" + 3) (nil) // Entry IDs that were in the Pending Entry List but no longer in the stream get a nil value. + 4) (nil) // These nil values will be dropped so that we can return a map value for the second response element. + + server version >= 7.0.0 example: + 1) "0-0" + 2) 1) 1) "1-0" + 2) 1) "field1" + 2) "value1" + 3) "field2" + 4) "value2" + 2) 1) "1-1" + 2) 1) "field3" + 2) "value3" + 3) 1) "1-2" // Entry IDs that were in the Pending Entry List but no longer in the stream are listed in the + 2) "1-3" // third response element, which is an array of these IDs. + */ + ExpectedReturnType::XAutoClaimReturnType => match value { + // Response will have 2 elements if server version < 7.0.0, and 3 elements otherwise. + Value::Array(mut array) if array.len() == 2 || array.len() == 3 => { + let mut result: Vec = Vec::with_capacity(array.len()); + // The first element is always a stream ID as a string, so the clone is cheap. + result.push(array[0].clone()); + + let mut stale_entry_ids: Option = None; + if array.len() == 3 { + // We use array.remove to avoid having to clone the other element(s). If we removed the second + // element before the third, the third would have to be shifted, so we remove the third element + // first to improve performance. + stale_entry_ids = Some(array.remove(2)); + } + + // Only the element at index 1 needs conversion. + result.push(convert_to_expected_type( + array.remove(1), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }) + )?); + + if let Some(value) = stale_entry_ids { + result.push(value); + } + + Ok(Value::Array(result)) + }, + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an XAUTOCLAIM response", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + }, + // `XINFO STREAM` returns nested maps with different types of data + /* RESP2 response example + + 1) "length" + 2) (integer) 2 + ... + 13) "recorded-first-entry-id" + 14) "1719710679916-0" + 15) "entries" + 16) 1) 1) "1719710679916-0" + 2) 1) "foo" + 2) "bar" + 3) "foo" + 4) "bar2" + 5) "some" + 6) "value" + 2) 1) "1719710688676-0" + 2) 1) "foo" + 2) "bar2" + 17) "groups" + 18) 1) 1) "name" + 2) "mygroup" + ... + 9) "pel-count" + 10) (integer) 2 + 11) "pending" + 12) 1) 1) "1719710679916-0" + 2) "Alice" + 3) (integer) 1719710707260 + 4) (integer) 1 + 2) 1) "1719710688676-0" + 2) "Alice" + 3) (integer) 1719710718373 + 4) (integer) 1 + 13) "consumers" + 14) 1) 1) "name" + 2) "Alice" + ... + 7) "pel-count" + 8) (integer) 2 + 9) "pending" + 10) 1) 1) "1719710679916-0" + 2) (integer) 1719710707260 + 3) (integer) 1 + 2) 1) "1719710688676-0" + 2) (integer) 1719710718373 + 3) (integer) 1 + + RESP3 response example + + 1# "length" => (integer) 2 + ... + 8# "entries" => + 1) 1) "1719710679916-0" + 2) 1) "foo" + 2) "bar" + 3) "foo" + 4) "bar2" + 5) "some" + 6) "value" + 2) 1) "1719710688676-0" + 2) 1) "foo" + 2) "bar2" + 9# "groups" => + 1) 1# "name" => "mygroup" + ... + 6# "pending" => + 1) 1) "1719710679916-0" + 2) "Alice" + 3) (integer) 1719710707260 + 4) (integer) 1 + 2) 1) "1719710688676-0" + 2) "Alice" + 3) (integer) 1719710718373 + 4) (integer) 1 + 7# "consumers" => + 1) 1# "name" => "Alice" + ... + 5# "pending" => + 1) 1) "1719710679916-0" + 2) (integer) 1719710707260 + 3) (integer) 1 + 2) 1) "1719710688676-0" + 2) (integer) 1719710718373 + 3) (integer) 1 + + Another RESP3 example on an empty stream + + 1# "length" => (integer) 0 + 2# "radix-tree-keys" => (integer) 0 + 3# "radix-tree-nodes" => (integer) 1 + 4# "last-generated-id" => "0-1" + 5# "max-deleted-entry-id" => "0-1" + 6# "entries-added" => (integer) 1 + 7# "recorded-first-entry-id" => "0-0" + 8# "entries" => (empty array) + 9# "groups" => (empty array) + + We want to convert the RESP2 format to RESP3, so we need to: + - convert any consumer in the consumer array to a map, if there are any consumers + - convert any group in the group array to a map, if there are any groups + - convert the root of the response into a map + */ + ExpectedReturnType::XInfoStreamFullReturnType => match value { + Value::Map(_) => Ok(value), // Response is already in RESP3 format - no conversion needed + Value::Array(mut array) => { + // Response is in RESP2 format. We need to convert to RESP3 format. + let groups_key = Value::SimpleString("groups".into()); + let opt_groups_key_index = array + .iter() + .position( + |key| { + let res = convert_to_expected_type(key.clone(), Some(ExpectedReturnType::SimpleString)); + match res { + Ok(converted_key) => { + converted_key == groups_key + }, + Err(_) => { + false + } + } + } + ); + + let Some(groups_key_index) = opt_groups_key_index else { + return Err((ErrorKind::TypeError, "No groups key found").into()); + }; + + let groups_value_index = groups_key_index + 1; + if array.get(groups_value_index).is_none() { + return Err((ErrorKind::TypeError, "No groups value found.").into()); + } + + let Value::Array(groups) = array[groups_value_index].clone() else { + return Err((ErrorKind::TypeError, "Incorrect value type received. Wanted an Array.").into()); + }; + + if groups.is_empty() { + let converted_response = convert_to_expected_type(Value::Array(array), Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None, + }))?; + + let Value::Map(map) = converted_response else { + return Err((ErrorKind::TypeError, "Incorrect value type received. Wanted a Map.").into()); + }; + + return Ok(Value::Map(map)); + } + + let mut groups_as_maps = Vec::new(); + for group_value in &groups { + let Value::Array(mut group) = group_value.clone() else { + return Err((ErrorKind::TypeError, "Incorrect value type received for group value. Wanted an Array").into()); + }; + + let consumers_key = Value::SimpleString("consumers".into()); + let opt_consumers_key_index = group + .iter() + .position( + |key| { + let res = convert_to_expected_type(key.clone(), Some(ExpectedReturnType::SimpleString)); + match res { + Ok(converted_key) => { + converted_key == consumers_key + }, + Err(_) => { + false + } + } + } + ); + + let Some(consumers_key_index) = opt_consumers_key_index else { + return Err((ErrorKind::TypeError, "No consumers key found").into()); + }; + + let consumers_value_index = consumers_key_index + 1; + if group.get(consumers_value_index).is_none() { + return Err((ErrorKind::TypeError, "No consumers value found.").into()); + } + + let Value::Array(ref consumers) = group[consumers_value_index] else { + return Err((ErrorKind::TypeError, "Incorrect value type received for consumers. Wanted an Array.").into()); + }; + + if consumers.is_empty() { + groups_as_maps.push( + convert_to_expected_type(Value::Array(group.clone()), Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None, + }))? + ); + continue; + } + + let mut consumers_as_maps = Vec::new(); + for consumer in consumers { + consumers_as_maps.push(convert_to_expected_type(consumer.clone(), Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None, + }))?); + } + + group[consumers_value_index] = Value::Array(consumers_as_maps); + let group_map = convert_to_expected_type(Value::Array(group), Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None, + }))?; + groups_as_maps.push(group_map); + } + + array[groups_value_index] = Value::Array(groups_as_maps); + let converted_response = convert_to_expected_type(Value::Array(array.to_vec()), Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None, + }))?; + + let Value::Map(map) = converted_response else { + return Err((ErrorKind::TypeError, "Incorrect value type received for response. Wanted a Map.").into()); + }; + + Ok(Value::Map(map)) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to XInfoStreamFullReturnType", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()), + } } } -fn convert_array_to_map( +/// Similar to [`convert_array_to_map_by_type`], but converts keys and values to the given types inside the map. +/// The input data is [`Value::Map`] payload, the output is the new [`Value::Map`]. +fn convert_map_entries( + map: Vec<(Value, Value)>, + key_type: Option, + value_type: Option, +) -> RedisResult { + let result = map + .into_iter() + .map(|(key, inner_value)| { + let converted_key = convert_to_expected_type(key, key_type)?; + let converted_value = convert_to_expected_type(inner_value, value_type)?; + Ok((converted_key, converted_value)) + }) + .collect::>(); + + result.map(Value::Map) +} + +/// Convert string returned by `LOLWUT` command. +/// The input string is shell-friendly and contains color codes and escape sequences. +/// The output string is user-friendly, colored whitespaces replaced with corresponding symbols. +fn convert_lolwut_string(data: &str) -> String { + if data.contains("\x1b[0m") { + data.replace("\x1b[0;97;107m \x1b[0m", "\u{2591}") + .replace("\x1b[0;37;47m \x1b[0m", "\u{2592}") + .replace("\x1b[0;90;100m \x1b[0m", "\u{2593}") + .replace("\x1b[0;30;40m \x1b[0m", " ") + } else { + data.to_owned() + } +} + +/// Converts elements in an array to the specified type. +/// +/// `array` is an array of values. +/// `element_type` is the type that the array elements should be converted to. +fn convert_array_elements( + array: Vec, + element_type: ExpectedReturnType, +) -> RedisResult { + let converted_array = array + .iter() + .map(|v| convert_to_expected_type(v.clone(), Some(element_type)).unwrap()) + .collect(); + Ok(Value::Array(converted_array)) +} + +/// Converts an array of flat maps into an array of maps. +/// Input: +/// ```text +/// 1) 1) "map 1 key 1" +/// 2) "map 1 value 1" +/// 3) "map 1 key 2" +/// 4) "map 1 value 2" +/// ... +/// 2) 1) "map 2 key 1" +/// 2) "map 2 value 1" +/// ... +/// ``` +/// Output: +/// ```text +/// 1) 1# "map 1 key 1" => "map 1 value 1" +/// 2# "map 1 key 2" => "map 1 value 2" +/// ... +/// 2) 1# "map 2 key 1" => "map 2 value 1" +/// ... +/// ``` +/// +/// `array` is an array of arrays, where each inner array represents data for a map. The inner arrays contain map keys at even-positioned elements and map values at odd-positioned elements. +/// `value_expected_return_type` is the desired type for the map values. +fn convert_array_of_flat_maps( array: Vec, - key_expected_return_type: Option, value_expected_return_type: Option, +) -> RedisResult { + let mut result: Vec = Vec::with_capacity(array.len()); + for entry in array { + let Value::Array(entry_as_array) = entry else { + return Err((ErrorKind::TypeError, "Incorrect value type received").into()); + }; + let map = convert_array_to_map_by_type( + entry_as_array, + Some(ExpectedReturnType::BulkString), + value_expected_return_type, + )?; + result.push(map); + } + Ok(Value::Array(result)) +} + +/// Converts key-value elements in a given map using the specified types. +/// +/// `map` A vector of key-values. +/// `key_type` is used to convert each key when collecting into the resulting map. +/// If `None` is given, then the key is not converted. +/// `value_type` is used to convert each value when collecting into the resulting map. +/// If `None` is given, then the value is not converted. +fn convert_inner_map_by_type( + map: Vec<(Value, Value)>, + key_type: Option, + value_type: Option, +) -> RedisResult { + let result = map + .into_iter() + .map(|(key, inner_value)| { + Ok(( + convert_to_expected_type(key, key_type)?, + convert_to_expected_type(inner_value, value_type)?, + )) + }) + .collect::>(); + + result.map(Value::Map) +} + +/// Converts the given array into a map, and converts key-value elements using the specified types. +/// +/// `array` Aa 2-dimensional array. Each entry of the array has two values: the first +/// element is the key for the map, and the second element is the value for the map. +/// `key_type` is used to convert each key when collecting into the resulting map. +/// If `None` is given, then the key is not converted. +/// `value_type` is used to convert each value when collecting into the resulting map. +/// If `None` is given, then the value is not converted. +fn convert_array_to_map_by_type( + array: Vec, + key_type: Option, + value_type: Option, ) -> RedisResult { let mut map = Vec::new(); let mut iterator = array.into_iter(); @@ -202,12 +1055,17 @@ fn convert_array_to_map( let Some(inner_value) = inner_iterator.next() else { return Err((ErrorKind::TypeError, "Missing value inside array of map").into()); }; - map.push(( - convert_to_expected_type(inner_key, key_expected_return_type)?, - convert_to_expected_type(inner_value, value_expected_return_type)?, + convert_to_expected_type(inner_key, key_type)?, + convert_to_expected_type(inner_value, value_type)?, )); } + Value::Nil => { + // Ignore nil key values - they will not be placed in the map. This is necessary for commands like + // XAUTOCLAIM, which can contain an array representation of a map with nil keys in place of stream IDs + // that existed in the Pending Entries List but no longer existed in the stream. + continue; + } _ => { let Some(value) = iterator.next() else { return Err(( @@ -217,54 +1075,1371 @@ fn convert_array_to_map( .into()); }; map.push(( - convert_to_expected_type(key, key_expected_return_type)?, - convert_to_expected_type(value, value_expected_return_type)?, + convert_to_expected_type(key, key_type)?, + convert_to_expected_type(value, value_type)?, )); } } } - Ok(Value::Map(map)) -} + Ok(Value::Map(map)) +} + +/// Used by commands like ZRANDMEMBER and HRANDFIELD. Normally a map would be more suitable for these key-value responses, but these commands may return duplicate key-value pairs depending on the command arguments. These duplicated pairs cannot be represented by a map. +/// +/// Converts a server response as follows: +/// - if the server returned an empty array, return an empty array. +/// - if the server returned a flat array (RESP2), convert it to a two-dimensional array, where the inner arrays are length=2 arrays representing key-value pairs. +/// - if the server returned a two-dimensional array (RESP3), return the response as is, since it is already in the correct format. +/// - otherwise, return an error. +/// +/// `response` is a server response that we should attempt to convert as described above. +/// `value_expected_return_type` indicates the desired return type of the values in the key-value pairs. The values will only be converted if the response was a flat array, since RESP3 already returns an array of pairs with values already of the correct type. +fn convert_to_array_of_pairs( + response: Value, + value_expected_return_type: Option, +) -> RedisResult { + match response { + Value::Nil => Ok(response), + Value::Array(ref array) if array.is_empty() || matches!(array[0], Value::Array(_)) => { + // The server response is an empty array or a RESP3 array of pairs. In RESP3, the values in the pairs are + // already of the correct type, so we do not need to convert them and `response` is in the correct format. + Ok(response) + } + Value::Array(array) + if array.len() % 2 == 0 + && matches!(array[0], Value::BulkString(_) | Value::SimpleString(_)) => + { + // The server response is a RESP2 flat array with keys at even indices and their associated values at + // odd indices. + convert_flat_array_to_array_of_pairs(array, value_expected_return_type) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of key-value pairs", + format!("(response was {:?})", get_value_type(&response)), + ) + .into()), + } +} + +/// Converts a flat array of values to a two-dimensional array, where the inner arrays are length=2 arrays representing key-value pairs. Normally a map would be more suitable for these responses, but some commands (eg HRANDFIELD) may return duplicate key-value pairs depending on the command arguments. These duplicated pairs cannot be represented by a map. +/// +/// `array` is a flat array containing keys at even-positioned elements and their associated values at odd-positioned elements. +/// `value_expected_return_type` indicates the desired return type of the values in the key-value pairs. +fn convert_flat_array_to_array_of_pairs( + array: Vec, + value_expected_return_type: Option, +) -> RedisResult { + if array.len() % 2 != 0 { + return Err(( + ErrorKind::TypeError, + "Response has odd number of items, and cannot be converted to an array of key-value pairs" + ) + .into()); + } + + let mut result = Vec::with_capacity(array.len() / 2); + for i in (0..array.len()).step_by(2) { + let key = array[i].clone(); + let value = convert_to_expected_type(array[i + 1].clone(), value_expected_return_type)?; + let pair = vec![key, value]; + result.push(Value::Array(pair)); + } + Ok(Value::Array(result)) +} + +pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { + let command = cmd.command()?; + + // TODO use enum to avoid mistakes + match command.as_slice() { + b"HGETALL" | b"CONFIG GET" | b"FT.CONFIG GET" | b"HELLO" => Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }), + b"XCLAIM" => { + if cmd.position(b"JUSTID").is_some() { + Some(ExpectedReturnType::ArrayOfStrings) + } else { + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::SimpleString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }) + } + } + b"XAUTOCLAIM" => { + if cmd.position(b"JUSTID").is_some() { + // Value conversion is not needed if the JUSTID arg was passed. + None + } else { + Some(ExpectedReturnType::XAutoClaimReturnType) + } + } + b"XINFO GROUPS" | b"XINFO CONSUMERS" => Some(ExpectedReturnType::ArrayOfMaps(&None)), + b"XRANGE" | b"XREVRANGE" => Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }), + b"XREAD" | b"XREADGROUP" => Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }), + }), + b"LCS" => cmd.position(b"IDX").map(|_| ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::SimpleString), + value_type: &None, + }), + b"INCRBYFLOAT" | b"HINCRBYFLOAT" | b"ZINCRBY" => Some(ExpectedReturnType::Double), + b"HEXISTS" + | b"HSETNX" + | b"EXPIRE" + | b"EXPIREAT" + | b"PEXPIRE" + | b"PEXPIREAT" + | b"SISMEMBER" + | b"PERSIST" + | b"SMOVE" + | b"RENAMENX" + | b"MOVE" + | b"COPY" + | b"MSETNX" + | b"XGROUP DESTROY" + | b"XGROUP CREATECONSUMER" => Some(ExpectedReturnType::Boolean), + b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), + b"SMEMBERS" | b"SINTER" | b"SDIFF" | b"SUNION" => Some(ExpectedReturnType::Set), + b"ZSCORE" | b"GEODIST" => Some(ExpectedReturnType::DoubleOrNull), + b"ZMSCORE" => Some(ExpectedReturnType::ArrayOfDoubleOrNull), + b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble), + b"BZMPOP" | b"ZMPOP" => Some(ExpectedReturnType::ZMPopReturnType), + b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType), + b"GEOPOS" => Some(ExpectedReturnType::ArrayOfArraysOfDoubleOrNull), + b"LMPOP" => Some(ExpectedReturnType::ArrayOfStringAndArrays), + b"BLMPOP" => Some(ExpectedReturnType::ArrayOfStringAndArrays), + b"HRANDFIELD" => cmd + .position(b"WITHVALUES") + .map(|_| ExpectedReturnType::ArrayOfPairs), + b"ZRANDMEMBER" => cmd + .position(b"WITHSCORES") + .map(|_| ExpectedReturnType::ArrayOfMemberScorePairs), + b"ZADD" => cmd + .position(b"INCR") + .map(|_| ExpectedReturnType::DoubleOrNull), + b"ZRANGE" | b"ZDIFF" | b"ZUNION" | b"ZINTER" => cmd + .position(b"WITHSCORES") + .map(|_| ExpectedReturnType::MapOfStringToDouble), + b"ZRANK" | b"ZREVRANK" => cmd + .position(b"WITHSCORE") + .map(|_| ExpectedReturnType::ZRankReturnType), + b"BZPOPMIN" | b"BZPOPMAX" => Some(ExpectedReturnType::KeyWithMemberAndScore), + b"SPOP" => { + if cmd.arg_idx(2).is_some() { + Some(ExpectedReturnType::Set) + } else { + None + } + } + b"LOLWUT" => Some(ExpectedReturnType::Lolwut), + b"FUNCTION LIST" => Some(ExpectedReturnType::ArrayOfMaps(&Some( + ExpectedReturnType::ArrayOfMaps(&Some(ExpectedReturnType::StringOrSet)), + ))), + b"FUNCTION STATS" => Some(ExpectedReturnType::FunctionStatsReturnType), + b"GEOSEARCH" => { + if cmd.position(b"WITHDIST").is_some() + || cmd.position(b"WITHHASH").is_some() + || cmd.position(b"WITHCOORD").is_some() + { + Some(ExpectedReturnType::GeoSearchReturnType) + } else { + None + } + } + b"XINFO STREAM" => { + if cmd.position(b"FULL").is_some() { + Some(ExpectedReturnType::XInfoStreamFullReturnType) + } else { + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None, + }) + } + } + _ => None, + } +} + +/// Gets the enum variant as a string for the `value` given. +pub(crate) fn get_value_type<'a>(value: &Value) -> &'a str { + match value { + Value::Nil => "Nil", + Value::Int(_) => "Int", + Value::BulkString(_) => "BulkString", + Value::Array(_) => "Array", + Value::SimpleString(_) => "SimpleString", + Value::Okay => "OK", + Value::Map(_) => "Map", + Value::Attribute { .. } => "Attribute", + Value::Set(_) => "Set", + Value::Double(_) => "Double", + Value::Boolean(_) => "Boolean", + Value::VerbatimString { .. } => "VerbatimString", + Value::BigNumber(_) => "BigNumber", + Value::Push { .. } => "Push", + // TODO Value::ServerError from https://github.com/redis-rs/redis-rs/pull/1093 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn xinfo_stream_expected_return_type() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("XINFO").arg("STREAM").arg("key")), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &None + }) + )); + + assert!(matches!( + expected_type_for_cmd(redis::cmd("XINFO").arg("STREAM").arg("key").arg("FULL")), + Some(ExpectedReturnType::XInfoStreamFullReturnType) + )); + } + + #[test] + fn convert_xinfo_stream() { + // Only a partial response is represented here for brevity - the rest of the response follows the same format. + let groups_resp2_response = Value::Array(vec![ + Value::BulkString("length".to_string().into_bytes()), + Value::Int(2), + Value::BulkString("entries".to_string().into_bytes()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString("1-0".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("a".to_string().into_bytes()), + Value::BulkString("b".to_string().into_bytes()), + Value::BulkString("c".to_string().into_bytes()), + Value::BulkString("d".to_string().into_bytes()), + ]), + ])]), + Value::BulkString("groups".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("group1".to_string().into_bytes()), + Value::BulkString("consumers".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("consumer1".to_string().into_bytes()), + Value::BulkString("pending".to_string().into_bytes()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString("1-0".to_string().into_bytes()), + Value::Int(1), + ])]), + ]), + Value::Array(vec![ + Value::BulkString("pending".to_string().into_bytes()), + Value::Array(vec![]), + ]), + ]), + ]), + Value::Array(vec![ + Value::BulkString("consumers".to_string().into_bytes()), + Value::Array(vec![]), + ]), + ]), + ]); + + let groups_resp3_response = Value::Map(vec![ + ( + Value::BulkString("length".to_string().into_bytes()), + Value::Int(2), + ), + ( + Value::BulkString("entries".to_string().into_bytes()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString("1-0".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("a".to_string().into_bytes()), + Value::BulkString("b".to_string().into_bytes()), + Value::BulkString("c".to_string().into_bytes()), + Value::BulkString("d".to_string().into_bytes()), + ]), + ])]), + ), + ( + Value::BulkString("groups".to_string().into_bytes()), + Value::Array(vec![ + Value::Map(vec![ + ( + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("group1".to_string().into_bytes()), + ), + ( + Value::BulkString("consumers".to_string().into_bytes()), + Value::Array(vec![ + Value::Map(vec![ + ( + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("consumer1".to_string().into_bytes()), + ), + ( + Value::BulkString("pending".to_string().into_bytes()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString("1-0".to_string().into_bytes()), + Value::Int(1), + ])]), + ), + ]), + Value::Map(vec![( + Value::BulkString("pending".to_string().into_bytes()), + Value::Array(vec![]), + )]), + ]), + ), + ]), + Value::Map(vec![( + Value::BulkString("consumers".to_string().into_bytes()), + Value::Array(vec![]), + )]), + ]), + ), + ]); + + // We want the RESP2 response to be converted into RESP3 format. + assert_eq!( + convert_to_expected_type( + groups_resp2_response.clone(), + Some(ExpectedReturnType::XInfoStreamFullReturnType) + ) + .unwrap(), + groups_resp3_response.clone() + ); + + // RESP3 responses are already in the correct format and should not change format. + assert_eq!( + convert_to_expected_type( + groups_resp3_response.clone(), + Some(ExpectedReturnType::XInfoStreamFullReturnType) + ) + .unwrap(), + groups_resp3_response.clone() + ); + + let resp2_empty_groups = Value::Array(vec![ + Value::BulkString("groups".to_string().into_bytes()), + Value::Array(vec![]), + ]); + + let resp3_empty_groups = Value::Map(vec![( + Value::BulkString("groups".to_string().into_bytes()), + Value::Array(vec![]), + )]); + + // We want the RESP2 response to be converted into RESP3 format. + assert_eq!( + convert_to_expected_type( + resp2_empty_groups.clone(), + Some(ExpectedReturnType::XInfoStreamFullReturnType) + ) + .unwrap(), + resp3_empty_groups.clone() + ); + + // RESP3 responses are already in the correct format and should not change format. + assert_eq!( + convert_to_expected_type( + resp3_empty_groups.clone(), + Some(ExpectedReturnType::XInfoStreamFullReturnType) + ) + .unwrap(), + resp3_empty_groups.clone() + ); + } + + #[test] + fn xinfo_groups_xinfo_consumers_expected_return_type() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("XINFO").arg("GROUPS").arg("key")), + Some(ExpectedReturnType::ArrayOfMaps(&None)) + )); + + assert!(matches!( + expected_type_for_cmd(redis::cmd("XINFO").arg("CONSUMERS").arg("key").arg("group")), + Some(ExpectedReturnType::ArrayOfMaps(&None)) + )); + } + + #[test] + fn convert_xinfo_groups_xinfo_consumers() { + // The format of the XINFO GROUPS and XINFO CONSUMERS responses are essentially the same, so we only need to + // test one of them here. Only a partial response is represented here for brevity - the rest of the response + // follows the same format. + let groups_resp2_response = Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("mygroup".to_string().into_bytes()), + Value::BulkString("lag".to_string().into_bytes()), + Value::Int(0), + ]), + Value::Array(vec![ + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("some-other-group".to_string().into_bytes()), + Value::BulkString("lag".to_string().into_bytes()), + Value::Nil, + ]), + ]); + + let groups_resp3_response = Value::Array(vec![ + Value::Map(vec![ + ( + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("mygroup".to_string().into_bytes()), + ), + ( + Value::BulkString("lag".to_string().into_bytes()), + Value::Int(0), + ), + ]), + Value::Map(vec![ + ( + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("some-other-group".to_string().into_bytes()), + ), + ( + Value::BulkString("lag".to_string().into_bytes()), + Value::Nil, + ), + ]), + ]); + + // We want the RESP2 response to be converted into RESP3 format. + assert_eq!( + convert_to_expected_type( + groups_resp2_response.clone(), + Some(ExpectedReturnType::ArrayOfMaps(&None)) + ) + .unwrap(), + groups_resp3_response.clone() + ); + + // RESP3 responses are already in the correct format and should not be converted. + assert_eq!( + convert_to_expected_type( + groups_resp3_response.clone(), + Some(ExpectedReturnType::ArrayOfMaps(&None)) + ) + .unwrap(), + groups_resp3_response.clone() + ); + } + + #[test] + fn convert_function_list() { + let command = &mut redis::cmd("FUNCTION"); + command.arg("LIST"); + let expected_type = expected_type_for_cmd(command); + + assert!(matches!( + expected_type, + Some(ExpectedReturnType::ArrayOfMaps(_)) + )); + + let resp2_response = Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("library_name".to_string().into_bytes()), + Value::BulkString("mylib1".to_string().into_bytes()), + Value::BulkString("engine".to_string().into_bytes()), + Value::BulkString("LUA".to_string().into_bytes()), + Value::BulkString("functions".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("myfunc1".to_string().into_bytes()), + Value::BulkString("description".to_string().into_bytes()), + Value::Nil, + Value::BulkString("flags".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("read".to_string().into_bytes()), + Value::BulkString("write".to_string().into_bytes()), + ]), + ]), + Value::Array(vec![ + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("myfunc2".to_string().into_bytes()), + Value::BulkString("description".to_string().into_bytes()), + Value::BulkString("blahblah".to_string().into_bytes()), + Value::BulkString("flags".to_string().into_bytes()), + Value::Array(vec![]), + ]), + ]), + ]), + Value::Array(vec![ + Value::BulkString("library_name".to_string().into_bytes()), + Value::BulkString("mylib2".to_string().into_bytes()), + Value::BulkString("engine".to_string().into_bytes()), + Value::BulkString("LUA".to_string().into_bytes()), + Value::BulkString("functions".to_string().into_bytes()), + Value::Array(vec![]), + Value::BulkString("library_code".to_string().into_bytes()), + Value::BulkString("".to_string().into_bytes()), + ]), + ]); + + let resp3_response = Value::Array(vec![ + Value::Map(vec![ + ( + Value::BulkString("library_name".to_string().into_bytes()), + Value::BulkString("mylib1".to_string().into_bytes()), + ), + ( + Value::BulkString("engine".to_string().into_bytes()), + Value::BulkString("LUA".to_string().into_bytes()), + ), + ( + Value::BulkString("functions".to_string().into_bytes()), + Value::Array(vec![ + Value::Map(vec![ + ( + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("myfunc1".to_string().into_bytes()), + ), + ( + Value::BulkString("description".to_string().into_bytes()), + Value::Nil, + ), + ( + Value::BulkString("flags".to_string().into_bytes()), + Value::Set(vec![ + Value::BulkString("read".to_string().into_bytes()), + Value::BulkString("write".to_string().into_bytes()), + ]), + ), + ]), + Value::Map(vec![ + ( + Value::BulkString("name".to_string().into_bytes()), + Value::BulkString("myfunc2".to_string().into_bytes()), + ), + ( + Value::BulkString("description".to_string().into_bytes()), + Value::BulkString("blahblah".to_string().into_bytes()), + ), + ( + Value::BulkString("flags".to_string().into_bytes()), + Value::Set(vec![]), + ), + ]), + ]), + ), + ]), + Value::Map(vec![ + ( + Value::BulkString("library_name".to_string().into_bytes()), + Value::BulkString("mylib2".to_string().into_bytes()), + ), + ( + Value::BulkString("engine".to_string().into_bytes()), + Value::BulkString("LUA".to_string().into_bytes()), + ), + ( + Value::BulkString("functions".to_string().into_bytes()), + Value::Array(vec![]), + ), + ( + Value::BulkString("library_code".to_string().into_bytes()), + Value::BulkString("".to_string().into_bytes()), + ), + ]), + ]); + + let resp2_cluster_response = Value::Map(vec![ + (Value::BulkString("node1".into()), resp2_response.clone()), + (Value::BulkString("node2".into()), resp2_response.clone()), + (Value::BulkString("node3".into()), resp2_response.clone()), + ]); + + let resp3_cluster_response = Value::Map(vec![ + (Value::BulkString("node1".into()), resp3_response.clone()), + (Value::BulkString("node2".into()), resp3_response.clone()), + (Value::BulkString("node3".into()), resp3_response.clone()), + ]); + + // convert RESP2 -> RESP3 + assert_eq!( + convert_to_expected_type(resp2_response.clone(), expected_type).unwrap(), + resp3_response.clone() + ); + + // convert RESP3 -> RESP3 + assert_eq!( + convert_to_expected_type(resp3_response.clone(), expected_type).unwrap(), + resp3_response.clone() + ); + + // convert cluster RESP2 -> RESP3 + assert_eq!( + convert_to_expected_type(resp2_cluster_response.clone(), expected_type).unwrap(), + resp3_cluster_response.clone() + ); + + // convert cluster RESP3 -> RESP3 + assert_eq!( + convert_to_expected_type(resp3_cluster_response.clone(), expected_type).unwrap(), + resp3_cluster_response.clone() + ); + } + + #[test] + fn convert_lolwut() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("LOLWUT").arg("version").arg("42")), + Some(ExpectedReturnType::Lolwut) + )); + + let unconverted_string : String = "\x1b[0;97;107m \x1b[0m--\x1b[0;37;47m \x1b[0m--\x1b[0;90;100m \x1b[0m--\x1b[0;30;40m \x1b[0m".into(); + let expected: String = "\u{2591}--\u{2592}--\u{2593}-- ".into(); + + let converted_1 = convert_to_expected_type( + Value::BulkString(unconverted_string.clone().into_bytes()), + Some(ExpectedReturnType::Lolwut), + ); + assert_eq!( + Value::BulkString(expected.clone().into_bytes()), + converted_1.unwrap() + ); + + let converted_2 = convert_to_expected_type( + Value::VerbatimString { + format: redis::VerbatimFormat::Text, + text: unconverted_string.clone(), + }, + Some(ExpectedReturnType::Lolwut), + ); + assert_eq!( + Value::BulkString(expected.clone().into_bytes()), + converted_2.unwrap() + ); + + let converted_3 = convert_to_expected_type( + Value::Map(vec![ + ( + Value::SimpleString("node 1".into()), + Value::BulkString(unconverted_string.clone().into_bytes()), + ), + ( + Value::SimpleString("node 2".into()), + Value::BulkString(unconverted_string.clone().into_bytes()), + ), + ]), + Some(ExpectedReturnType::Lolwut), + ); + assert_eq!( + Value::Map(vec![ + ( + Value::BulkString("node 1".into()), + Value::BulkString(expected.clone().into_bytes()) + ), + ( + Value::BulkString("node 2".into()), + Value::BulkString(expected.clone().into_bytes()) + ), + ]), + converted_3.unwrap() + ); + + let converted_4 = convert_to_expected_type( + Value::SimpleString(unconverted_string.clone()), + Some(ExpectedReturnType::Lolwut), + ); + assert!(converted_4.is_err()); + } + + #[test] + fn convert_xclaim() { + assert!(matches!( + expected_type_for_cmd( + redis::cmd("XCLAIM") + .arg("key") + .arg("grou") + .arg("consumer") + .arg("0") + .arg("id") + ), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::SimpleString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }) + )); + assert!(matches!( + expected_type_for_cmd( + redis::cmd("XCLAIM") + .arg("key") + .arg("grou") + .arg("consumer") + .arg("0") + .arg("id") + .arg("JUSTID") + ), + Some(ExpectedReturnType::ArrayOfStrings) + )); + } + + #[test] + fn convert_xrange_xrevrange() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("XRANGE").arg("key").arg("start").arg("end")), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }) + )); + assert!(matches!( + expected_type_for_cmd(redis::cmd("XREVRANGE").arg("key").arg("end").arg("start")), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }) + )); + } + + #[test] + fn convert_xread() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("XREAD").arg("streams").arg("key").arg("id")), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }), + }) + )); + } + + #[test] + fn convert_xreadgroup() { + assert!(matches!( + expected_type_for_cmd( + redis::cmd("XREADGROUP") + .arg("GROUP") + .arg("group") + .arg("consumer") + .arg("streams") + .arg("key") + .arg("id") + ), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::BulkString), + value_type: &Some(ExpectedReturnType::ArrayOfPairs), + }), + }) + )); + } + + #[test] + fn convert_xautoclaim() { + // Value conversion is not needed if the JUSTID arg was passed. + assert!(expected_type_for_cmd( + redis::cmd("XAUTOCLAIM") + .arg("key") + .arg("group") + .arg("consumer") + .arg("0") + .arg("0-0") + .arg("JUSTID") + ) + .is_none()); + + assert!(matches!( + expected_type_for_cmd( + redis::cmd("XAUTOCLAIM") + .arg("key") + .arg("group") + .arg("consumer") + .arg("0") + .arg("0-0") + ), + Some(ExpectedReturnType::XAutoClaimReturnType) + )); + + let v6_response = Value::Array(vec![ + Value::BulkString("0-0".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("1-0".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("field1".to_string().into_bytes()), + Value::BulkString("value1".to_string().into_bytes()), + Value::BulkString("field2".to_string().into_bytes()), + Value::BulkString("value2".to_string().into_bytes()), + ]), + ]), + Value::Nil, // Entry IDs that were in the Pending Entry List but no longer in the stream get a nil value. + Value::Array(vec![ + Value::BulkString("1-1".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("field3".to_string().into_bytes()), + Value::BulkString("value3".to_string().into_bytes()), + ]), + ]), + ]), + ]); + + let expected_v6_response = Value::Array(vec![ + Value::BulkString("0-0".to_string().into_bytes()), + Value::Map(vec![ + ( + Value::BulkString("1-0".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("field1".to_string().into_bytes()), + Value::BulkString("value1".to_string().into_bytes()), + ]), + Value::Array(vec![ + Value::BulkString("field2".to_string().into_bytes()), + Value::BulkString("value2".to_string().into_bytes()), + ]), + ]), + ), + ( + Value::BulkString("1-1".to_string().into_bytes()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString("field3".to_string().into_bytes()), + Value::BulkString("value3".to_string().into_bytes()), + ])]), + ), + ]), + ]); + + assert_eq!( + convert_to_expected_type( + v6_response.clone(), + Some(ExpectedReturnType::XAutoClaimReturnType) + ) + .unwrap(), + expected_v6_response.clone() + ); + + let v7_response = Value::Array(vec![ + Value::BulkString("0-0".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("1-0".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("field1".to_string().into_bytes()), + Value::BulkString("value1".to_string().into_bytes()), + Value::BulkString("field2".to_string().into_bytes()), + Value::BulkString("value2".to_string().into_bytes()), + ]), + ]), + Value::Array(vec![ + Value::BulkString("1-1".to_string().into_bytes()), + Value::Array(vec![ + Value::BulkString("field3".to_string().into_bytes()), + Value::BulkString("value3".to_string().into_bytes()), + ]), + ]), + ]), + Value::Array(vec![Value::BulkString("1-2".to_string().into_bytes())]), + ]); + + let expected_v7_response = Value::Array(vec![ + Value::BulkString("0-0".to_string().into_bytes()), + Value::Map(vec![ + ( + Value::BulkString("1-0".to_string().into_bytes()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString("field1".to_string().into_bytes()), + Value::BulkString("value1".to_string().into_bytes()), + ]), + Value::Array(vec![ + Value::BulkString("field2".to_string().into_bytes()), + Value::BulkString("value2".to_string().into_bytes()), + ]), + ]), + ), + ( + Value::BulkString("1-1".to_string().into_bytes()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString("field3".to_string().into_bytes()), + Value::BulkString("value3".to_string().into_bytes()), + ])]), + ), + ]), + Value::Array(vec![Value::BulkString("1-2".to_string().into_bytes())]), + ]); + + assert_eq!( + convert_to_expected_type( + v7_response.clone(), + Some(ExpectedReturnType::XAutoClaimReturnType) + ) + .unwrap(), + expected_v7_response.clone() + ); + } + + #[test] + fn test_convert_empty_array_to_map_is_nil() { + let mut cmd = redis::cmd("XREAD"); + let expected_type = expected_type_for_cmd(cmd.arg("STREAMS").arg("key").arg("id")); + + // test convert nil is OK + assert_eq!( + convert_to_expected_type(Value::Nil, expected_type,), + Ok(Value::Nil) + ); + } + + #[test] + fn test_convert_array_to_map_with_none() { + let unconverted_map = vec![ + ( + Value::BulkString(b"key1".to_vec()), + Value::BulkString(b"10.5".to_vec()), + ), + (Value::Double(20.5), Value::Double(19.5)), + (Value::Double(18.5), Value::BulkString(b"30.2".to_vec())), + ]; + + let converted_type = ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }; + let converted_map = + convert_to_expected_type(Value::Map(unconverted_map), Some(converted_type)).unwrap(); + + let converted_map = if let Value::Map(map) = converted_map { + map + } else { + panic!("Expected a Map, but got {:?}", converted_map); + }; + + assert_eq!(converted_map.len(), 3); + + let (key, value) = &converted_map[0]; + assert_eq!(*key, Value::BulkString(b"key1".to_vec())); + assert_eq!(*value, Value::BulkString(b"10.5".to_vec())); + + let (key, value) = &converted_map[1]; + assert_eq!(*key, Value::Double(20.5)); + assert_eq!(*value, Value::Double(19.5)); + + let (key, value) = &converted_map[2]; + assert_eq!(*key, Value::Double(18.5)); + assert_eq!(*value, Value::BulkString(b"30.2".to_vec())); + } + + #[test] + fn test_convert_2d_array_to_map_using_expected_return_type_map() { + // in RESP2, we get an array of arrays value like this: + // 1) 1) "key1" + // 2) 1) 1) "streamid-1" + // 2) 1) "f1" + // 2) "v1" + // 3) "f2" + // 4) "v2" + // 2) 1) "streamid-2" ... + // 2) 1) "key2"... + // + let array_of_arrays = vec![ + Value::Array(vec![ + Value::BulkString(b"key1".to_vec()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString(b"streamid-1".to_vec()), + Value::Array(vec![ + Value::BulkString(b"field1".to_vec()), + Value::BulkString(b"value1".to_vec()), + ]), + ])]), + ]), + Value::Array(vec![ + Value::BulkString(b"key2".to_vec()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"streamid-2".to_vec()), + Value::Array(vec![ + Value::BulkString(b"field21".to_vec()), + Value::BulkString(b"value21".to_vec()), + Value::BulkString(b"field22".to_vec()), + Value::BulkString(b"value22".to_vec()), + ]), + ]), + Value::Array(vec![ + Value::BulkString(b"streamid-3".to_vec()), + Value::Array(vec![ + Value::BulkString(b"field3".to_vec()), + Value::BulkString(b"value3".to_vec()), + ]), + ]), + ]), + ]), + ]; + + // convert to a map value like this: + // #1) "key1" + // #1) "streamid-1" + // 1) "f1" + // 2) "v1" + // 3) "f2" + // 4) "v2" + // #2) "streamid-2" + // ... + // #2) "key2" + // ... + let mut cmd = redis::cmd("XREAD"); + let expected_type = expected_type_for_cmd(cmd.arg("STREAMS").arg("key").arg("id")); + let converted_map = + convert_to_expected_type(Value::Array(array_of_arrays), expected_type).unwrap(); + + let converted_map = if let Value::Map(map) = converted_map { + map + } else { + panic!("Expected a Map, but got {:?}", converted_map); + }; + // expect 2 keys + assert_eq!(converted_map.len(), 2); + + let (key, value) = &converted_map[0]; + assert_eq!(Value::BulkString(b"key1".to_vec()), *key); + assert_eq!( + Value::Map(vec![( + Value::BulkString(b"streamid-1".to_vec()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString(b"field1".to_vec()), + Value::BulkString(b"value1".to_vec()), + ]),]), + ),]), + *value, + ); + + let (key, value) = &converted_map[1]; + assert_eq!(*key, Value::BulkString(b"key2".to_vec())); + assert_eq!( + Value::Map(vec![ + ( + Value::BulkString(b"streamid-2".to_vec()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"field21".to_vec()), + Value::BulkString(b"value21".to_vec()), + ]), + Value::Array(vec![ + Value::BulkString(b"field22".to_vec()), + Value::BulkString(b"value22".to_vec()), + ]), + ]), + ), + ( + Value::BulkString(b"streamid-3".to_vec()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString(b"field3".to_vec()), + Value::BulkString(b"value3".to_vec()), + ]),]), + ), + ]), + *value, + ); + } + + #[test] + fn test_convert_map_with_inner_array_to_map_of_maps_using_expected_return_type_map() { + // in RESP3, we get a map of arrays value like this: + // 1# "key1" => + // 1) 1) "streamid-1" + // 2) 1) "f1" + // 2) "v1" + // 3) "f2" + // 4) "v2" + // 2) 1) "streamid-2" ... + // 2# "key2" => ... + // + let map_of_arrays = vec![ + ( + Value::BulkString("key1".into()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString(b"streamid-1".to_vec()), + Value::Array(vec![ + Value::BulkString(b"field1".to_vec()), + Value::BulkString(b"value1".to_vec()), + ]), + ])]), + ), + ( + Value::BulkString("key2".into()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"streamid-2".to_vec()), + Value::Array(vec![ + Value::BulkString(b"field21".to_vec()), + Value::BulkString(b"value21".to_vec()), + Value::BulkString(b"field22".to_vec()), + Value::BulkString(b"value22".to_vec()), + ]), + ]), + Value::Array(vec![ + Value::BulkString(b"streamid-3".to_vec()), + Value::Array(vec![ + Value::BulkString(b"field3".to_vec()), + Value::BulkString(b"value3".to_vec()), + ]), + ]), + ]), + ), + ]; + + // convert to a map value like this: + // #1) "key1" + // #1) "streamid-1" + // 1) "f1" + // 2) "v1" + // 3) "f2" + // 4) "v2" + // #2) "streamid-2" + // ... + // #2) "key2" + // ... + let mut cmd = redis::cmd("XREAD"); + let expected_type = expected_type_for_cmd(cmd.arg("STREAMS").arg("key").arg("id")); + let converted_map = + convert_to_expected_type(Value::Map(map_of_arrays), expected_type).unwrap(); + + let converted_map = if let Value::Map(map) = converted_map { + map + } else { + panic!("Expected a Map, but got {:?}", converted_map); + }; + + assert_eq!(converted_map.len(), 2); + + let (key, value) = &converted_map[0]; + assert_eq!(Value::BulkString(b"key1".to_vec()), *key); + assert_eq!( + Value::Map(vec![( + Value::BulkString(b"streamid-1".to_vec()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString(b"field1".to_vec()), + Value::BulkString(b"value1".to_vec()), + ]),]), + ),]), + *value, + ); + + let (key, value) = &converted_map[1]; + assert_eq!(*key, Value::BulkString(b"key2".to_vec())); + assert_eq!( + Value::Map(vec![ + ( + Value::BulkString(b"streamid-2".to_vec()), + Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"field21".to_vec()), + Value::BulkString(b"value21".to_vec()), + ]), + Value::Array(vec![ + Value::BulkString(b"field22".to_vec()), + Value::BulkString(b"value22".to_vec()), + ]), + ]), + ), + ( + Value::BulkString(b"streamid-3".to_vec()), + Value::Array(vec![Value::Array(vec![ + Value::BulkString(b"field3".to_vec()), + Value::BulkString(b"value3".to_vec()), + ]),]), + ), + ]), + *value, + ); + } -pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { - let command = cmd.command()?; + #[test] + fn convert_function_stats() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("FUNCTION").arg("STATS")), + Some(ExpectedReturnType::FunctionStatsReturnType) + )); - // TODO use enum to avoid mistakes - match command.as_slice() { - b"HGETALL" | b"XREAD" | b"CONFIG GET" | b"FT.CONFIG GET" | b"HELLO" => { - Some(ExpectedReturnType::Map) - } - b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double), - b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" - | b"SISMEMBER" | b"PERSIST" | b"SMOVE" => Some(ExpectedReturnType::Boolean), - b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), - b"SMEMBERS" | b"SINTER" => Some(ExpectedReturnType::Set), - b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull), - b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble), - b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType), - b"ZADD" => cmd - .position(b"INCR") - .map(|_| ExpectedReturnType::DoubleOrNull), - b"ZRANGE" | b"ZDIFF" => cmd - .position(b"WITHSCORES") - .map(|_| ExpectedReturnType::MapOfStringToDouble), - b"ZRANK" | b"ZREVRANK" => cmd - .position(b"WITHSCORE") - .map(|_| ExpectedReturnType::ZrankReturnType), - b"SPOP" => { - if cmd.arg_idx(2).is_some() { - Some(ExpectedReturnType::Set) - } else { - None - } - } - _ => None, - } -} + let resp2_response_non_empty_first_part_data = vec![ + Value::BulkString(b"running_script".into()), + Value::Array(vec![ + Value::BulkString(b"name".into()), + Value::BulkString(b"".into()), + Value::BulkString(b"command".into()), + Value::Array(vec![ + Value::BulkString(b"fcall".into()), + Value::BulkString(b"".into()), + Value::BulkString(b"... rest `fcall` args ...".into()), + ]), + Value::BulkString(b"duration_ms".into()), + Value::Int(24529), + ]), + ]; -#[cfg(test)] -mod tests { - use super::*; + let resp2_response_empty_first_part_data = + vec![Value::BulkString(b"running_script".into()), Value::Nil]; + + let resp2_response_second_part_data = vec![ + Value::BulkString(b"engines".into()), + Value::Array(vec![ + Value::BulkString(b"LUA".into()), + Value::Array(vec![ + Value::BulkString(b"libraries_count".into()), + Value::Int(3), + Value::BulkString(b"functions_count".into()), + Value::Int(5), + ]), + ]), + ]; + let resp2_response_with_non_empty_first_part = Value::Array( + [ + resp2_response_non_empty_first_part_data.clone(), + resp2_response_second_part_data.clone(), + ] + .concat(), + ); + + let resp2_response_with_empty_first_part = Value::Array( + [ + resp2_response_empty_first_part_data.clone(), + resp2_response_second_part_data.clone(), + ] + .concat(), + ); + + let resp2_cluster_response = Value::Map(vec![ + ( + Value::BulkString(b"node1".into()), + resp2_response_with_non_empty_first_part.clone(), + ), + ( + Value::BulkString(b"node2".into()), + resp2_response_with_empty_first_part.clone(), + ), + ( + Value::BulkString(b"node3".into()), + resp2_response_with_empty_first_part.clone(), + ), + ]); + + let resp3_response_non_empty_first_part_data = vec![( + Value::BulkString(b"running_script".into()), + Value::Map(vec![ + ( + Value::BulkString(b"name".into()), + Value::BulkString(b"".into()), + ), + ( + Value::BulkString(b"command".into()), + Value::Array(vec![ + Value::BulkString(b"fcall".into()), + Value::BulkString(b"".into()), + Value::BulkString(b"... rest `fcall` args ...".into()), + ]), + ), + (Value::BulkString(b"duration_ms".into()), Value::Int(24529)), + ]), + )]; + + let resp3_response_empty_first_part_data = + vec![(Value::BulkString(b"running_script".into()), Value::Nil)]; + + let resp3_response_second_part_data = vec![( + Value::BulkString(b"engines".into()), + Value::Map(vec![( + Value::BulkString(b"LUA".into()), + Value::Map(vec![ + (Value::BulkString(b"libraries_count".into()), Value::Int(3)), + (Value::BulkString(b"functions_count".into()), Value::Int(5)), + ]), + )]), + )]; + + let resp3_response_with_non_empty_first_part = Value::Map( + [ + resp3_response_non_empty_first_part_data.clone(), + resp3_response_second_part_data.clone(), + ] + .concat(), + ); + + let resp3_response_with_empty_first_part = Value::Map( + [ + resp3_response_empty_first_part_data.clone(), + resp3_response_second_part_data.clone(), + ] + .concat(), + ); + + let resp3_cluster_response = Value::Map(vec![ + ( + Value::BulkString(b"node1".into()), + resp3_response_with_non_empty_first_part.clone(), + ), + ( + Value::BulkString(b"node2".into()), + resp3_response_with_empty_first_part.clone(), + ), + ( + Value::BulkString(b"node3".into()), + resp3_response_with_empty_first_part.clone(), + ), + ]); + + let conversion_type = Some(ExpectedReturnType::FunctionStatsReturnType); + // resp2 -> resp3 conversion with non-empty `running_script` block + assert_eq!( + convert_to_expected_type( + resp2_response_with_non_empty_first_part.clone(), + conversion_type + ), + Ok(resp3_response_with_non_empty_first_part.clone()) + ); + // resp2 -> resp3 conversion with empty `running_script` block + assert_eq!( + convert_to_expected_type( + resp2_response_with_empty_first_part.clone(), + conversion_type + ), + Ok(resp3_response_with_empty_first_part.clone()) + ); + // resp2 -> resp3 cluster response + assert_eq!( + convert_to_expected_type(resp2_cluster_response.clone(), conversion_type), + Ok(resp3_cluster_response.clone()) + ); + // resp3 -> resp3 conversion with non-empty `running_script` block + assert_eq!( + convert_to_expected_type( + resp3_response_with_non_empty_first_part.clone(), + conversion_type + ), + Ok(resp3_response_with_non_empty_first_part.clone()) + ); + // resp3 -> resp3 conversion with empty `running_script` block + assert_eq!( + convert_to_expected_type( + resp3_response_with_empty_first_part.clone(), + conversion_type + ), + Ok(resp3_response_with_empty_first_part.clone()) + ); + // resp3 -> resp3 cluster response + assert_eq!( + convert_to_expected_type(resp3_cluster_response.clone(), conversion_type), + Ok(resp3_cluster_response.clone()) + ); + } #[test] fn convert_smismember() { @@ -273,14 +2448,164 @@ mod tests { Some(ExpectedReturnType::ArrayOfBools) )); - let redis_response = Value::Array(vec![Value::Int(0), Value::Int(1)]); + let response = Value::Array(vec![Value::Int(0), Value::Int(1)]); let converted_response = - convert_to_expected_type(redis_response, Some(ExpectedReturnType::ArrayOfBools)) - .unwrap(); + convert_to_expected_type(response, Some(ExpectedReturnType::ArrayOfBools)).unwrap(); let expected_response = Value::Array(vec![Value::Boolean(false), Value::Boolean(true)]); assert_eq!(expected_response, converted_response); } + #[test] + fn convert_to_array_of_pairs_return_type() { + assert!(matches!( + expected_type_for_cmd( + redis::cmd("HRANDFIELD") + .arg("key") + .arg("1") + .arg("withvalues") + ), + Some(ExpectedReturnType::ArrayOfPairs) + )); + + assert!(expected_type_for_cmd(redis::cmd("HRANDFIELD").arg("key").arg("1")).is_none()); + assert!(expected_type_for_cmd(redis::cmd("HRANDFIELD").arg("key")).is_none()); + + let flat_array = Value::Array(vec![ + Value::BulkString(b"key1".to_vec()), + Value::BulkString(b"value1".to_vec()), + Value::BulkString(b"key2".to_vec()), + Value::BulkString(b"value2".to_vec()), + ]); + let two_dimensional_array = Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"key1".to_vec()), + Value::BulkString(b"value1".to_vec()), + ]), + Value::Array(vec![ + Value::BulkString(b"key2".to_vec()), + Value::BulkString(b"value2".to_vec()), + ]), + ]); + let converted_flat_array = + convert_to_expected_type(flat_array, Some(ExpectedReturnType::ArrayOfPairs)).unwrap(); + assert_eq!(two_dimensional_array, converted_flat_array); + + let converted_two_dimensional_array = convert_to_expected_type( + two_dimensional_array.clone(), + Some(ExpectedReturnType::ArrayOfPairs), + ) + .unwrap(); + assert_eq!(two_dimensional_array, converted_two_dimensional_array); + + let empty_array = Value::Array(vec![]); + let converted_empty_array = + convert_to_expected_type(empty_array.clone(), Some(ExpectedReturnType::ArrayOfPairs)) + .unwrap(); + assert_eq!(empty_array, converted_empty_array); + + let flat_array_unexpected_length = + Value::Array(vec![Value::BulkString(b"somekey".to_vec())]); + assert!(convert_to_expected_type( + flat_array_unexpected_length, + Some(ExpectedReturnType::ArrayOfPairs) + ) + .is_err()); + } + + #[test] + fn convert_zmpop_response() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("BZMPOP").arg(1).arg(1).arg("key").arg("min")), + Some(ExpectedReturnType::ZMPopReturnType) + )); + assert!(matches!( + expected_type_for_cmd(redis::cmd("ZMPOP").arg(1).arg(1).arg("key").arg("min")), + Some(ExpectedReturnType::ZMPopReturnType) + )); + + let response = Value::Array(vec![ + Value::SimpleString("key".into()), + Value::Array(vec![ + Value::Array(vec![Value::SimpleString("elem1".into()), Value::Double(1.)]), + Value::Array(vec![Value::SimpleString("elem2".into()), Value::Double(2.)]), + ]), + ]); + let converted_response = + convert_to_expected_type(response, Some(ExpectedReturnType::ZMPopReturnType)).unwrap(); + let expected_response = Value::Array(vec![ + Value::SimpleString("key".into()), + Value::Map(vec![ + (Value::BulkString("elem1".into()), Value::Double(1.)), + (Value::BulkString("elem2".into()), Value::Double(2.)), + ]), + ]); + assert_eq!(expected_response, converted_response); + + let response = Value::Nil; + let converted_response = + convert_to_expected_type(response.clone(), Some(ExpectedReturnType::ZMPopReturnType)) + .unwrap(); + assert_eq!(response, converted_response); + } + + #[test] + fn convert_to_member_score_pairs_return_type() { + assert!(matches!( + expected_type_for_cmd( + redis::cmd("ZRANDMEMBER") + .arg("key") + .arg("1") + .arg("withscores") + ), + Some(ExpectedReturnType::ArrayOfMemberScorePairs) + )); + + assert!(expected_type_for_cmd(redis::cmd("ZRANDMEMBER").arg("key").arg("1")).is_none()); + assert!(expected_type_for_cmd(redis::cmd("ZRANDMEMBER").arg("key")).is_none()); + + // convert_to_array_of_pairs_return_type already tests most functionality since the conversion for ArrayOfPairs + // and ArrayOfMemberScorePairs is mostly the same. Here we also test that the scores are converted to double + // when the server response was a RESP2 flat array. + let flat_array = Value::Array(vec![ + Value::BulkString(b"one".to_vec()), + Value::BulkString(b"1.0".to_vec()), + Value::BulkString(b"two".to_vec()), + Value::BulkString(b"2.0".to_vec()), + ]); + let expected_response = Value::Array(vec![ + Value::Array(vec![Value::BulkString(b"one".to_vec()), Value::Double(1.0)]), + Value::Array(vec![Value::BulkString(b"two".to_vec()), Value::Double(2.0)]), + ]); + let converted_flat_array = convert_to_expected_type( + flat_array, + Some(ExpectedReturnType::ArrayOfMemberScorePairs), + ) + .unwrap(); + assert_eq!(expected_response, converted_flat_array); + } + + #[test] + fn convert_to_array_of_string_and_array_return_type() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("LMPOP").arg("1").arg("key").arg("LEFT")), + Some(ExpectedReturnType::ArrayOfStringAndArrays) + )); + + // testing value conversion + let flat_array = Value::Array(vec![ + Value::BulkString(b"1".to_vec()), + Value::Array(vec![Value::BulkString(b"one".to_vec())]), + ]); + let expected_response = Value::Map(vec![( + Value::BulkString("1".into()), + Value::Array(vec![Value::BulkString(b"one".to_vec())]), + )]); + let converted_flat_array = + convert_to_expected_type(flat_array, Some(ExpectedReturnType::ArrayOfStringAndArrays)) + .unwrap(); + assert_eq!(expected_response, converted_flat_array); + } + #[test] fn convert_zadd_only_if_incr_is_included() { assert!(matches!( @@ -318,6 +2643,105 @@ mod tests { assert!(expected_type_for_cmd(redis::cmd("ZDIFF").arg("1")).is_none()); } + #[test] + fn convert_zunion_only_if_withscores_is_included() { + // Test ZUNION without options + assert!(matches!( + expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("WITHSCORES") + ), + Some(ExpectedReturnType::MapOfStringToDouble) + )); + + assert!( + expected_type_for_cmd(redis::cmd("ZUNION").arg("2").arg("set1").arg("set2")).is_none() + ); + + // Test ZUNION with Weights + assert!(matches!( + expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("WEIGHTS") + .arg("1") + .arg("2") + .arg("WITHSCORES") + ), + Some(ExpectedReturnType::MapOfStringToDouble) + )); + + assert!(expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("WEIGHTS") + .arg("1") + .arg("2") + ) + .is_none()); + + // Test ZUNION with Aggregate + assert!(matches!( + expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("AGGREGATE") + .arg("MAX") + .arg("WITHSCORES") + ), + Some(ExpectedReturnType::MapOfStringToDouble) + )); + + assert!(expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("AGGREGATE") + .arg("MAX") + ) + .is_none()); + + // Test ZUNION with Weights and Aggregate + assert!(matches!( + expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("WEIGHTS") + .arg("1") + .arg("2") + .arg("AGGREGATE") + .arg("MAX") + .arg("WITHSCORES") + ), + Some(ExpectedReturnType::MapOfStringToDouble) + )); + + assert!(expected_type_for_cmd( + redis::cmd("ZUNION") + .arg("2") + .arg("set1") + .arg("set2") + .arg("WEIGHTS") + .arg("1") + .arg("2") + .arg("AGGREGATE") + .arg("MAX") + ) + .is_none()); + } + #[test] fn zpopmin_zpopmax_return_type() { assert!(matches!( @@ -331,6 +2755,70 @@ mod tests { )); } + #[test] + fn convert_bzpopmin_bzpopmax() { + assert!(matches!( + expected_type_for_cmd( + redis::cmd("BZPOPMIN") + .arg("myzset1") + .arg("myzset2") + .arg("1") + ), + Some(ExpectedReturnType::KeyWithMemberAndScore) + )); + + assert!(matches!( + expected_type_for_cmd( + redis::cmd("BZPOPMAX") + .arg("myzset1") + .arg("myzset2") + .arg("1") + ), + Some(ExpectedReturnType::KeyWithMemberAndScore) + )); + + let array_with_double_score = Value::Array(vec![ + Value::BulkString(b"key1".to_vec()), + Value::BulkString(b"member1".to_vec()), + Value::Double(2.0), + ]); + let result = convert_to_expected_type( + array_with_double_score.clone(), + Some(ExpectedReturnType::KeyWithMemberAndScore), + ) + .unwrap(); + assert_eq!(array_with_double_score, result); + + let array_with_string_score = Value::Array(vec![ + Value::BulkString(b"key1".to_vec()), + Value::BulkString(b"member1".to_vec()), + Value::BulkString(b"2.0".to_vec()), + ]); + let result = convert_to_expected_type( + array_with_string_score.clone(), + Some(ExpectedReturnType::KeyWithMemberAndScore), + ) + .unwrap(); + assert_eq!(array_with_double_score, result); + + let converted_nil_value = + convert_to_expected_type(Value::Nil, Some(ExpectedReturnType::KeyWithMemberAndScore)) + .unwrap(); + assert_eq!(Value::Nil, converted_nil_value); + + let array_with_unexpected_length = Value::Array(vec![ + Value::BulkString(b"key1".to_vec()), + Value::BulkString(b"member1".to_vec()), + Value::Double(2.0), + Value::Double(2.0), + ]); + assert!(convert_to_expected_type( + array_with_unexpected_length, + Some(ExpectedReturnType::KeyWithMemberAndScore) + ) + .is_err()); + } + #[test] fn convert_zank_zrevrank_only_if_withsocres_is_included() { assert!(matches!( @@ -340,7 +2828,7 @@ mod tests { .arg("member") .arg("withscore") ), - Some(ExpectedReturnType::ZrankReturnType) + Some(ExpectedReturnType::ZRankReturnType) )); assert!(expected_type_for_cmd(redis::cmd("zrank").arg("key").arg("member")).is_none()); @@ -352,12 +2840,41 @@ mod tests { .arg("member") .arg("withscore") ), - Some(ExpectedReturnType::ZrankReturnType) + Some(ExpectedReturnType::ZRankReturnType) )); assert!(expected_type_for_cmd(redis::cmd("ZREVRANK").arg("key").arg("member")).is_none()); } + #[test] + fn convert_zmscore() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("ZMSCORE").arg("key").arg("member")), + Some(ExpectedReturnType::ArrayOfDoubleOrNull) + )); + + let array_response = Value::Array(vec![ + Value::Nil, + Value::Double(1.5), + Value::BulkString(b"2.5".to_vec()), + ]); + let converted_response = convert_to_expected_type( + array_response, + Some(ExpectedReturnType::ArrayOfDoubleOrNull), + ) + .unwrap(); + let expected_response = + Value::Array(vec![Value::Nil, Value::Double(1.5), Value::Double(2.5)]); + assert_eq!(expected_response, converted_response); + + let unexpected_response_type = Value::Double(0.5); + assert!(convert_to_expected_type( + unexpected_response_type, + Some(ExpectedReturnType::ArrayOfDoubleOrNull) + ) + .is_err()); + } + #[test] fn convert_smove_to_bool() { assert!(matches!( @@ -372,7 +2889,7 @@ mod tests { convert_to_expected_type(Value::Nil, Some(ExpectedReturnType::MapOfStringToDouble)), Ok(Value::Nil) ); - let redis_map = vec![ + let unconverted_map = vec![ ( Value::BulkString(b"key1".to_vec()), Value::BulkString(b"10.5".to_vec()), @@ -385,7 +2902,7 @@ mod tests { ]; let converted_map = convert_to_expected_type( - Value::Map(redis_map), + Value::Map(unconverted_map), Some(ExpectedReturnType::MapOfStringToDouble), ) .unwrap(); @@ -459,7 +2976,7 @@ mod tests { #[test] fn test_convert_to_zrank_return_type() { assert_eq!( - convert_to_expected_type(Value::Nil, Some(ExpectedReturnType::ZrankReturnType)), + convert_to_expected_type(Value::Nil, Some(ExpectedReturnType::ZRankReturnType)), Ok(Value::Nil) ); @@ -470,7 +2987,7 @@ mod tests { let array_result = convert_to_expected_type( Value::Array(array), - Some(ExpectedReturnType::ZrankReturnType), + Some(ExpectedReturnType::ZRankReturnType), ) .unwrap(); @@ -487,7 +3004,7 @@ mod tests { let array_err = vec![Value::BulkString(b"key".to_vec())]; assert!(convert_to_expected_type( Value::Array(array_err), - Some(ExpectedReturnType::ZrankReturnType) + Some(ExpectedReturnType::ZRankReturnType) ) .is_err()); } @@ -536,4 +3053,92 @@ mod tests { )); assert!(expected_type_for_cmd(redis::cmd("SPOP").arg("key1")).is_none()); } + #[test] + fn test_convert_to_geo_search_return_type() { + let array = Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"name1".to_vec()), + Value::BulkString(b"1.23".to_vec()), // dist (float) + Value::Int(123456), // hash (int) + Value::Array(vec![ + Value::BulkString(b"10.0".to_vec()), // lon (float) + Value::BulkString(b"20.0".to_vec()), // lat (float) + ]), + ]), + Value::Array(vec![ + Value::BulkString(b"name2".to_vec()), + Value::BulkString(b"2.34".to_vec()), // dist (float) + Value::Int(654321), // hash (int) + Value::Array(vec![ + Value::BulkString(b"30.0".to_vec()), // lon (float) + Value::BulkString(b"40.0".to_vec()), // lat (float) + ]), + ]), + ]); + + // Expected output value after conversion + let expected_result = Value::Array(vec![ + Value::Array(vec![ + Value::BulkString(b"name1".to_vec()), + Value::Array(vec![ + Value::Double(1.23), // dist (float) + Value::Int(123456), // hash (int) + Value::Array(vec![ + Value::Double(10.0), // lon (float) + Value::Double(20.0), // lat (float) + ]), + ]), + ]), + Value::Array(vec![ + Value::BulkString(b"name2".to_vec()), + Value::Array(vec![ + Value::Double(2.34), // dist (float) + Value::Int(654321), // hash (int) + Value::Array(vec![ + Value::Double(30.0), // lon (float) + Value::Double(40.0), // lat (float) + ]), + ]), + ]), + ]); + + let result = + convert_to_expected_type(array.clone(), Some(ExpectedReturnType::GeoSearchReturnType)) + .unwrap(); + assert_eq!(result, expected_result); + } + #[test] + fn test_geosearch_return_type() { + assert!(matches!( + expected_type_for_cmd( + redis::cmd("GEOSEARCH") + .arg("WITHDIST") + .arg("WITHHASH") + .arg("WITHCOORD") + ), + Some(ExpectedReturnType::GeoSearchReturnType) + )); + + assert!(matches!( + expected_type_for_cmd(redis::cmd("GEOSEARCH").arg("WITHDIST").arg("WITHHASH")), + Some(ExpectedReturnType::GeoSearchReturnType) + )); + + assert!(matches!( + expected_type_for_cmd(redis::cmd("GEOSEARCH").arg("WITHDIST")), + Some(ExpectedReturnType::GeoSearchReturnType) + )); + + assert!(expected_type_for_cmd(redis::cmd("GEOSEARCH").arg("key")).is_none()); + } + #[test] + fn convert_lcs_idx() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("LCS").arg("key1").arg("key2").arg("IDX")), + Some(ExpectedReturnType::Map { + key_type: &Some(ExpectedReturnType::SimpleString), + value_type: &None, + }) + )); + } } diff --git a/glide-core/src/cluster_scan_container.rs b/glide-core/src/cluster_scan_container.rs new file mode 100644 index 0000000000..38cb1ec76d --- /dev/null +++ b/glide-core/src/cluster_scan_container.rs @@ -0,0 +1,65 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ +use logger_core::log_debug; +use nanoid::nanoid; +use once_cell::sync::Lazy; +use redis::{RedisResult, ScanStateRC}; +use std::{collections::HashMap, sync::Mutex}; + +// This is a container for storing the cursor of a cluster scan. +// The cursor for a cluster scan is a ref to the actual ScanState struct in redis-rs. +// In order to avoid dropping it when it is passed between layers of the application, +// we store it in this container and only pass the id of the cursor. +// The cursor is stored in the container and can be retrieved using the id. +// In wrapper layer we wrap the id in an object, which, when dropped, trigger the removal of the cursor from the container. +// When the ref is removed from the container, the actual ScanState struct is dropped by Rust GC. + +static CONTAINER: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +pub fn insert_cluster_scan_cursor(scan_state: ScanStateRC) -> String { + let id = nanoid!(); + CONTAINER.lock().unwrap().insert(id.clone(), scan_state); + log_debug( + "scan_state_cursor insert", + format!( + "Inserted to container scan_state_cursor with id: `{:?}`", + id + ), + ); + id +} + +pub fn get_cluster_scan_cursor(id: String) -> RedisResult { + let scan_state_rc = CONTAINER.lock().unwrap().get(&id).cloned(); + log_debug( + "scan_state_cursor get", + format!( + "Retrieved from container scan_state_cursor with id: `{:?}`", + id + ), + ); + match scan_state_rc { + Some(scan_state_rc) => Ok(scan_state_rc), + None => Err(redis::RedisError::from(( + redis::ErrorKind::ResponseError, + "Invalid scan_state_cursor id", + format!( + "The scan_state_cursor sent with id: `{:?}` does not exist", + id + ), + ))), + } +} + +pub fn remove_scan_state_cursor(id: String) { + log_debug( + "scan_state_cursor remove", + format!( + "Removed from container scan_state_cursor with id: `{:?}`", + id + ), + ); + CONTAINER.lock().unwrap().remove(&id); +} diff --git a/glide-core/src/errors.rs b/glide-core/src/errors.rs index 1c05aad84b..b5f9b1af9e 100644 --- a/glide-core/src/errors.rs +++ b/glide-core/src/errors.rs @@ -1,5 +1,5 @@ /* - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use redis::RedisError; diff --git a/glide-core/src/lib.rs b/glide-core/src/lib.rs index f904928be1..8da08e99f9 100644 --- a/glide-core/src/lib.rs +++ b/glide-core/src/lib.rs @@ -1,5 +1,5 @@ /* - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ #[cfg(feature = "socket-layer")] @@ -15,4 +15,5 @@ pub use socket_listener::*; pub mod errors; pub mod scripts_container; pub use client::ConnectionRequest; +pub mod cluster_scan_container; pub mod request_type; diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/command_request.proto similarity index 52% rename from glide-core/src/protobuf/redis_request.proto rename to glide-core/src/protobuf/command_request.proto index e602c914e7..96d3f7ae43 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/command_request.proto @@ -1,5 +1,5 @@ syntax = "proto3"; -package redis_request; +package command_request; enum SimpleRoutes { AllNodes=0; @@ -41,10 +41,8 @@ enum RequestType { InvalidRequest = 0; /// An unknown command, where all arguments are defined by the user. CustomCommand = 1; - /// Type of a get string request. - GetString = 2; - /// Type of a set string request. - SetString = 3; + Get = 2; + Set = 3; Ping = 4; Info = 5; Del = 6; @@ -68,10 +66,10 @@ enum RequestType { ClientUnblock = 24; ClientUnpause = 25; Expire = 26; - HashSet = 27; - HashGet = 28; - HashDel = 29; - HashExists = 30; + HSet = 27; + HGet = 28; + HDel = 29; + HExists = 30; MGet=31; MSet=32; Incr=33; @@ -79,11 +77,11 @@ enum RequestType { Decr=35; IncrByFloat=36; DecrBy=37; - HashGetAll=38; - HashMSet=39; - HashMGet=40; - HashIncrBy = 41; - HashIncrByFloat = 42; + HGetAll=38; + HMSet=39; + HMGet=40; + HIncrBy = 41; + HIncrByFloat = 42; LPush = 43; LPop = 44; RPush = 45; @@ -102,11 +100,11 @@ enum RequestType { Exists = 58; Unlink = 59; TTL = 60; - Zadd = 61; - Zrem = 62; - Zrange = 63; - Zcard = 64; - Zcount = 65; + ZAdd = 61; + ZRem = 62; + ZRange = 63; + ZCard = 64; + ZCount = 65; ZIncrBy = 66; ZScore = 67; Type = 68; @@ -114,7 +112,7 @@ enum RequestType { Echo = 70; ZPopMin = 71; Strlen = 72; - Lindex = 73; + LIndex = 73; ZPopMax = 74; XRead = 75; XAdd = 76; @@ -125,22 +123,22 @@ enum RequestType { XGroupDestroy = 81; HSetNX = 82; SIsMember = 83; - Hvals = 84; + HVals = 84; PTTL = 85; ZRemRangeByRank = 86; Persist = 87; ZRemRangeByScore = 88; Time = 89; - Zrank = 90; + ZRank = 90; Rename = 91; DBSize = 92; - Brpop = 93; - Hkeys = 94; - Spop = 95; + BRPop = 93; + HKeys = 94; + SPop = 95; PfAdd = 96; PfCount = 97; PfMerge = 98; - Blpop = 100; + BLPop = 100; LInsert = 101; RPushX = 102; LPushX = 103; @@ -159,14 +157,102 @@ enum RequestType { GetRange = 116; SMove = 117; SMIsMember = 118; + ZUnionStore = 119; LastSave = 120; GeoAdd = 121; GeoHash = 122; + ObjectEncoding = 123; + SDiff = 124; + ObjectIdleTime = 125; + ObjectRefCount = 126; + Lolwut = 100500; + GeoDist = 127; + GeoPos = 128; + BZPopMax = 129; + ObjectFreq = 130; + RenameNX = 131; + Touch = 132; + ZRevRank = 133; + ZInterStore = 134; + HRandField = 135; + ZUnion = 136; + BZPopMin = 137; + FlushAll = 138; + ZRandMember = 139; + BitCount = 140; + BZMPop = 141; + SetBit = 142; + ZInterCard = 143; + ZMPop = 144; + GetBit = 145; + ZInter = 146; + BitPos = 147; + BitOp = 148; + HStrlen = 149; + FunctionLoad = 150; + FunctionList = 151; + FunctionDelete = 152; + FunctionFlush = 153; + FCall = 154; + LMPop = 155; + ExpireTime = 156; + PExpireTime = 157; + BLMPop = 158; + XLen = 159; + Sort = 160; + FunctionKill = 161; + FunctionStats = 162; + FCallReadOnly = 163; + FlushDB = 164; + LSet = 165; + XDel = 166; + XRange = 167; + LMove = 168; + BLMove = 169; + GetDel = 170; + SRandMember = 171; + BitField = 172; + BitFieldReadOnly = 173; + Move = 174; + SInterCard = 175; + XRevRange = 176; + Copy = 178; + MSetNX = 179; + LPos = 180; + LCS = 181; + GeoSearch = 182; + Watch = 183; + UnWatch = 184; + GeoSearchStore = 185; + SUnion = 186; + Publish = 187; + SPublish = 188; + XGroupCreateConsumer = 189; + XGroupDelConsumer = 190; + RandomKey = 191; + GetEx = 192; + Dump = 193; + Restore = 194; + SortReadOnly = 195; + FunctionDump = 196; + FunctionRestore = 197; + XPending = 198; + XGroupSetId = 199; + SScan = 200; + ZScan = 201; + HScan = 202; + XAutoClaim = 203; + XInfoGroups = 204; + XInfoConsumers = 205; + XInfoStream = 207; + Scan = 206; + Wait = 208; + XClaim = 209; } message Command { message ArgsArray { - repeated string args = 1; + repeated bytes args = 1; } RequestType request_type = 1; @@ -176,23 +262,39 @@ message Command { } } +// Used for script requests with large keys or args vectors +message ScriptInvocationPointers { + string hash = 1; + optional uint64 keys_pointer = 2; + optional uint64 args_pointer = 3; +} + message ScriptInvocation { string hash = 1; - repeated string keys = 2; - repeated string args = 3; + repeated bytes keys = 2; + repeated bytes args = 3; } message Transaction { repeated Command commands = 1; } -message RedisRequest { +message ClusterScan { + string cursor = 1; + optional bytes match_pattern = 2; + optional int64 count = 3; + optional string object_type = 4; +} + +message CommandRequest { uint32 callback_idx = 1; oneof command { Command single_command = 2; Transaction transaction = 3; ScriptInvocation script_invocation = 4; + ScriptInvocationPointers script_invocation_pointers = 5; + ClusterScan cluster_scan = 6; } - Routes route = 5; + Routes route = 7; } diff --git a/glide-core/src/protobuf/connection_request.proto b/glide-core/src/protobuf/connection_request.proto index ecdeeae1b2..a186a1f41f 100644 --- a/glide-core/src/protobuf/connection_request.proto +++ b/glide-core/src/protobuf/connection_request.proto @@ -36,6 +36,22 @@ message PeriodicChecksManualInterval { message PeriodicChecksDisabled { } +enum PubSubChannelType { + Exact = 0; + Pattern = 1; + Sharded = 2; +} + +message PubSubChannelsOrPatterns +{ + repeated bytes channels_or_patterns = 1; +} + +message PubSubSubscriptions +{ + map channels_or_patterns_by_type = 1; +} + // IMPORTANT - if you add fields here, you probably need to add them also in client/mod.rs:`sanitized_request_string`. message ConnectionRequest { repeated NodeAddress addresses = 1; @@ -52,6 +68,7 @@ message ConnectionRequest { PeriodicChecksManualInterval periodic_checks_manual_interval = 11; PeriodicChecksDisabled periodic_checks_disabled = 12; } + PubSubSubscriptions pubsub_subscriptions = 13; } message ConnectionRetryStrategy { diff --git a/glide-core/src/protobuf/response.proto b/glide-core/src/protobuf/response.proto index 33591112ba..871d38e476 100644 --- a/glide-core/src/protobuf/response.proto +++ b/glide-core/src/protobuf/response.proto @@ -21,6 +21,7 @@ message Response { RequestError request_error = 4; string closing_error = 5; } + bool is_push = 6; } enum ConstantResponse { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 3f0a83045d..cd511f73a2 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -1,18 +1,18 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use redis::{cmd, Cmd}; #[cfg(feature = "socket-layer")] -use crate::redis_request::RequestType as ProtobufRequestType; +use crate::command_request::RequestType as ProtobufRequestType; #[repr(C)] #[derive(Debug)] pub enum RequestType { InvalidRequest = 0, CustomCommand = 1, - GetString = 2, - SetString = 3, + Get = 2, + Set = 3, Ping = 4, Info = 5, Del = 6, @@ -36,10 +36,10 @@ pub enum RequestType { ClientUnblock = 24, ClientUnpause = 25, Expire = 26, - HashSet = 27, - HashGet = 28, - HashDel = 29, - HashExists = 30, + HSet = 27, + HGet = 28, + HDel = 29, + HExists = 30, MGet = 31, MSet = 32, Incr = 33, @@ -47,11 +47,11 @@ pub enum RequestType { Decr = 35, IncrByFloat = 36, DecrBy = 37, - HashGetAll = 38, - HashMSet = 39, - HashMGet = 40, - HashIncrBy = 41, - HashIncrByFloat = 42, + HGetAll = 38, + HMSet = 39, + HMGet = 40, + HIncrBy = 41, + HIncrByFloat = 42, LPush = 43, LPop = 44, RPush = 45, @@ -70,11 +70,11 @@ pub enum RequestType { Exists = 58, Unlink = 59, TTL = 60, - Zadd = 61, - Zrem = 62, - Zrange = 63, - Zcard = 64, - Zcount = 65, + ZAdd = 61, + ZRem = 62, + ZRange = 63, + ZCard = 64, + ZCount = 65, ZIncrBy = 66, ZScore = 67, Type = 68, @@ -82,7 +82,7 @@ pub enum RequestType { Echo = 70, ZPopMin = 71, Strlen = 72, - Lindex = 73, + LIndex = 73, ZPopMax = 74, XRead = 75, XAdd = 76, @@ -93,22 +93,22 @@ pub enum RequestType { XGroupDestroy = 81, HSetNX = 82, SIsMember = 83, - Hvals = 84, + HVals = 84, PTTL = 85, ZRemRangeByRank = 86, Persist = 87, ZRemRangeByScore = 88, Time = 89, - Zrank = 90, + ZRank = 90, Rename = 91, DBSize = 92, - Brpop = 93, - Hkeys = 94, - Spop = 95, + BRPop = 93, + HKeys = 94, + SPop = 95, PfAdd = 96, PfCount = 97, PfMerge = 98, - Blpop = 100, + BLPop = 100, LInsert = 101, RPushX = 102, LPushX = 103, @@ -127,9 +127,97 @@ pub enum RequestType { GetRange = 116, SMove = 117, SMIsMember = 118, + ZUnionStore = 119, LastSave = 120, GeoAdd = 121, GeoHash = 122, + ObjectEncoding = 123, + SDiff = 124, + ObjectIdleTime = 125, + ObjectRefCount = 126, + Lolwut = 100500, + GeoDist = 127, + GeoPos = 128, + BZPopMax = 129, + ObjectFreq = 130, + RenameNX = 131, + Touch = 132, + ZRevRank = 133, + ZInterStore = 134, + HRandField = 135, + ZUnion = 136, + BZPopMin = 137, + FlushAll = 138, + ZRandMember = 139, + BitCount = 140, + BZMPop = 141, + SetBit = 142, + ZInterCard = 143, + ZMPop = 144, + GetBit = 145, + ZInter = 146, + BitPos = 147, + BitOp = 148, + HStrlen = 149, + FunctionLoad = 150, + FunctionList = 151, + FunctionDelete = 152, + FunctionFlush = 153, + FCall = 154, + LMPop = 155, + ExpireTime = 156, + PExpireTime = 157, + BLMPop = 158, + XLen = 159, + Sort = 160, + FunctionKill = 161, + FunctionStats = 162, + FCallReadOnly = 163, + FlushDB = 164, + LSet = 165, + XDel = 166, + XRange = 167, + LMove = 168, + BLMove = 169, + GetDel = 170, + SRandMember = 171, + BitField = 172, + BitFieldReadOnly = 173, + Move = 174, + SInterCard = 175, + XRevRange = 176, + Copy = 178, + MSetNX = 179, + LPos = 180, + LCS = 181, + GeoSearch = 182, + Watch = 183, + UnWatch = 184, + GeoSearchStore = 185, + SUnion = 186, + Publish = 187, + SPublish = 188, + XGroupCreateConsumer = 189, + XGroupDelConsumer = 190, + RandomKey = 191, + GetEx = 192, + Dump = 193, + Restore = 194, + SortReadOnly = 195, + FunctionDump = 196, + FunctionRestore = 197, + XPending = 198, + XGroupSetId = 199, + SScan = 200, + ZScan = 201, + HScan = 202, + XAutoClaim = 203, + XInfoGroups = 204, + XInfoConsumers = 205, + XInfoStream = 207, + Scan = 206, + Wait = 208, + XClaim = 209, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -144,8 +232,8 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { match value.enum_value_or(ProtobufRequestType::InvalidRequest) { ProtobufRequestType::InvalidRequest => RequestType::InvalidRequest, ProtobufRequestType::CustomCommand => RequestType::CustomCommand, - ProtobufRequestType::GetString => RequestType::GetString, - ProtobufRequestType::SetString => RequestType::SetString, + ProtobufRequestType::Get => RequestType::Get, + ProtobufRequestType::Set => RequestType::Set, ProtobufRequestType::Ping => RequestType::Ping, ProtobufRequestType::Info => RequestType::Info, ProtobufRequestType::Del => RequestType::Del, @@ -169,10 +257,10 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ClientUnblock => RequestType::ClientUnblock, ProtobufRequestType::ClientUnpause => RequestType::ClientUnpause, ProtobufRequestType::Expire => RequestType::Expire, - ProtobufRequestType::HashSet => RequestType::HashSet, - ProtobufRequestType::HashGet => RequestType::HashGet, - ProtobufRequestType::HashDel => RequestType::HashDel, - ProtobufRequestType::HashExists => RequestType::HashExists, + ProtobufRequestType::HSet => RequestType::HSet, + ProtobufRequestType::HGet => RequestType::HGet, + ProtobufRequestType::HDel => RequestType::HDel, + ProtobufRequestType::HExists => RequestType::HExists, ProtobufRequestType::MSet => RequestType::MSet, ProtobufRequestType::MGet => RequestType::MGet, ProtobufRequestType::Incr => RequestType::Incr, @@ -180,11 +268,11 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::IncrByFloat => RequestType::IncrByFloat, ProtobufRequestType::Decr => RequestType::Decr, ProtobufRequestType::DecrBy => RequestType::DecrBy, - ProtobufRequestType::HashGetAll => RequestType::HashGetAll, - ProtobufRequestType::HashMSet => RequestType::HashMSet, - ProtobufRequestType::HashMGet => RequestType::HashMGet, - ProtobufRequestType::HashIncrBy => RequestType::HashIncrBy, - ProtobufRequestType::HashIncrByFloat => RequestType::HashIncrByFloat, + ProtobufRequestType::HGetAll => RequestType::HGetAll, + ProtobufRequestType::HMSet => RequestType::HMSet, + ProtobufRequestType::HMGet => RequestType::HMGet, + ProtobufRequestType::HIncrBy => RequestType::HIncrBy, + ProtobufRequestType::HIncrByFloat => RequestType::HIncrByFloat, ProtobufRequestType::LPush => RequestType::LPush, ProtobufRequestType::LPop => RequestType::LPop, ProtobufRequestType::RPush => RequestType::RPush, @@ -203,11 +291,11 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::Exists => RequestType::Exists, ProtobufRequestType::Unlink => RequestType::Unlink, ProtobufRequestType::TTL => RequestType::TTL, - ProtobufRequestType::Zadd => RequestType::Zadd, - ProtobufRequestType::Zrem => RequestType::Zrem, - ProtobufRequestType::Zrange => RequestType::Zrange, - ProtobufRequestType::Zcard => RequestType::Zcard, - ProtobufRequestType::Zcount => RequestType::Zcount, + ProtobufRequestType::ZAdd => RequestType::ZAdd, + ProtobufRequestType::ZRem => RequestType::ZRem, + ProtobufRequestType::ZRange => RequestType::ZRange, + ProtobufRequestType::ZCard => RequestType::ZCard, + ProtobufRequestType::ZCount => RequestType::ZCount, ProtobufRequestType::ZIncrBy => RequestType::ZIncrBy, ProtobufRequestType::ZScore => RequestType::ZScore, ProtobufRequestType::Type => RequestType::Type, @@ -215,7 +303,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::Echo => RequestType::Echo, ProtobufRequestType::ZPopMin => RequestType::ZPopMin, ProtobufRequestType::Strlen => RequestType::Strlen, - ProtobufRequestType::Lindex => RequestType::Lindex, + ProtobufRequestType::LIndex => RequestType::LIndex, ProtobufRequestType::ZPopMax => RequestType::ZPopMax, ProtobufRequestType::XAck => RequestType::XAck, ProtobufRequestType::XAdd => RequestType::XAdd, @@ -226,25 +314,25 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::XTrim => RequestType::XTrim, ProtobufRequestType::HSetNX => RequestType::HSetNX, ProtobufRequestType::SIsMember => RequestType::SIsMember, - ProtobufRequestType::Hvals => RequestType::Hvals, + ProtobufRequestType::HVals => RequestType::HVals, ProtobufRequestType::PTTL => RequestType::PTTL, ProtobufRequestType::ZRemRangeByRank => RequestType::ZRemRangeByRank, ProtobufRequestType::Persist => RequestType::Persist, ProtobufRequestType::ZRemRangeByScore => RequestType::ZRemRangeByScore, ProtobufRequestType::Time => RequestType::Time, - ProtobufRequestType::Zrank => RequestType::Zrank, + ProtobufRequestType::ZRank => RequestType::ZRank, ProtobufRequestType::Rename => RequestType::Rename, ProtobufRequestType::DBSize => RequestType::DBSize, - ProtobufRequestType::Brpop => RequestType::Brpop, - ProtobufRequestType::Hkeys => RequestType::Hkeys, + ProtobufRequestType::BRPop => RequestType::BRPop, + ProtobufRequestType::HKeys => RequestType::HKeys, ProtobufRequestType::PfAdd => RequestType::PfAdd, ProtobufRequestType::PfCount => RequestType::PfCount, ProtobufRequestType::PfMerge => RequestType::PfMerge, ProtobufRequestType::RPushX => RequestType::RPushX, ProtobufRequestType::LPushX => RequestType::LPushX, - ProtobufRequestType::Blpop => RequestType::Blpop, + ProtobufRequestType::BLPop => RequestType::BLPop, ProtobufRequestType::LInsert => RequestType::LInsert, - ProtobufRequestType::Spop => RequestType::Spop, + ProtobufRequestType::SPop => RequestType::SPop, ProtobufRequestType::ZMScore => RequestType::ZMScore, ProtobufRequestType::ZDiff => RequestType::ZDiff, ProtobufRequestType::ZDiffStore => RequestType::ZDiffStore, @@ -260,9 +348,97 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GetRange => RequestType::GetRange, ProtobufRequestType::SMove => RequestType::SMove, ProtobufRequestType::SMIsMember => RequestType::SMIsMember, + ProtobufRequestType::ZUnionStore => RequestType::ZUnionStore, ProtobufRequestType::LastSave => RequestType::LastSave, ProtobufRequestType::GeoAdd => RequestType::GeoAdd, ProtobufRequestType::GeoHash => RequestType::GeoHash, + ProtobufRequestType::ObjectEncoding => RequestType::ObjectEncoding, + ProtobufRequestType::ObjectFreq => RequestType::ObjectFreq, + ProtobufRequestType::ObjectIdleTime => RequestType::ObjectIdleTime, + ProtobufRequestType::GeoDist => RequestType::GeoDist, + ProtobufRequestType::SDiff => RequestType::SDiff, + ProtobufRequestType::ObjectRefCount => RequestType::ObjectRefCount, + ProtobufRequestType::Lolwut => RequestType::Lolwut, + ProtobufRequestType::GeoPos => RequestType::GeoPos, + ProtobufRequestType::BZPopMax => RequestType::BZPopMax, + ProtobufRequestType::RenameNX => RequestType::RenameNX, + ProtobufRequestType::Touch => RequestType::Touch, + ProtobufRequestType::ZRevRank => RequestType::ZRevRank, + ProtobufRequestType::ZInterStore => RequestType::ZInterStore, + ProtobufRequestType::HRandField => RequestType::HRandField, + ProtobufRequestType::ZUnion => RequestType::ZUnion, + ProtobufRequestType::BZPopMin => RequestType::BZPopMin, + ProtobufRequestType::FlushAll => RequestType::FlushAll, + ProtobufRequestType::ZRandMember => RequestType::ZRandMember, + ProtobufRequestType::BitCount => RequestType::BitCount, + ProtobufRequestType::BZMPop => RequestType::BZMPop, + ProtobufRequestType::SetBit => RequestType::SetBit, + ProtobufRequestType::LMPop => RequestType::LMPop, + ProtobufRequestType::BLMPop => RequestType::BLMPop, + ProtobufRequestType::ZInterCard => RequestType::ZInterCard, + ProtobufRequestType::ZMPop => RequestType::ZMPop, + ProtobufRequestType::GetBit => RequestType::GetBit, + ProtobufRequestType::ZInter => RequestType::ZInter, + ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad, + ProtobufRequestType::FunctionList => RequestType::FunctionList, + ProtobufRequestType::FunctionDelete => RequestType::FunctionDelete, + ProtobufRequestType::FunctionFlush => RequestType::FunctionFlush, + ProtobufRequestType::FCall => RequestType::FCall, + ProtobufRequestType::BitPos => RequestType::BitPos, + ProtobufRequestType::BitOp => RequestType::BitOp, + ProtobufRequestType::HStrlen => RequestType::HStrlen, + ProtobufRequestType::ExpireTime => RequestType::ExpireTime, + ProtobufRequestType::PExpireTime => RequestType::PExpireTime, + ProtobufRequestType::XLen => RequestType::XLen, + ProtobufRequestType::FunctionKill => RequestType::FunctionKill, + ProtobufRequestType::FunctionStats => RequestType::FunctionStats, + ProtobufRequestType::FCallReadOnly => RequestType::FCallReadOnly, + ProtobufRequestType::FlushDB => RequestType::FlushDB, + ProtobufRequestType::LSet => RequestType::LSet, + ProtobufRequestType::XDel => RequestType::XDel, + ProtobufRequestType::XRange => RequestType::XRange, + ProtobufRequestType::LMove => RequestType::LMove, + ProtobufRequestType::BLMove => RequestType::BLMove, + ProtobufRequestType::GetDel => RequestType::GetDel, + ProtobufRequestType::SRandMember => RequestType::SRandMember, + ProtobufRequestType::BitField => RequestType::BitField, + ProtobufRequestType::BitFieldReadOnly => RequestType::BitFieldReadOnly, + ProtobufRequestType::Move => RequestType::Move, + ProtobufRequestType::SInterCard => RequestType::SInterCard, + ProtobufRequestType::Copy => RequestType::Copy, + ProtobufRequestType::Sort => RequestType::Sort, + ProtobufRequestType::XRevRange => RequestType::XRevRange, + ProtobufRequestType::MSetNX => RequestType::MSetNX, + ProtobufRequestType::LPos => RequestType::LPos, + ProtobufRequestType::LCS => RequestType::LCS, + ProtobufRequestType::GeoSearch => RequestType::GeoSearch, + ProtobufRequestType::SUnion => RequestType::SUnion, + ProtobufRequestType::Watch => RequestType::Watch, + ProtobufRequestType::UnWatch => RequestType::UnWatch, + ProtobufRequestType::GeoSearchStore => RequestType::GeoSearchStore, + ProtobufRequestType::Publish => RequestType::Publish, + ProtobufRequestType::SPublish => RequestType::SPublish, + ProtobufRequestType::XGroupCreateConsumer => RequestType::XGroupCreateConsumer, + ProtobufRequestType::XGroupDelConsumer => RequestType::XGroupDelConsumer, + ProtobufRequestType::RandomKey => RequestType::RandomKey, + ProtobufRequestType::GetEx => RequestType::GetEx, + ProtobufRequestType::Dump => RequestType::Dump, + ProtobufRequestType::Restore => RequestType::Restore, + ProtobufRequestType::SortReadOnly => RequestType::SortReadOnly, + ProtobufRequestType::FunctionDump => RequestType::FunctionDump, + ProtobufRequestType::FunctionRestore => RequestType::FunctionRestore, + ProtobufRequestType::XPending => RequestType::XPending, + ProtobufRequestType::XGroupSetId => RequestType::XGroupSetId, + ProtobufRequestType::SScan => RequestType::SScan, + ProtobufRequestType::ZScan => RequestType::ZScan, + ProtobufRequestType::HScan => RequestType::HScan, + ProtobufRequestType::XAutoClaim => RequestType::XAutoClaim, + ProtobufRequestType::XInfoGroups => RequestType::XInfoGroups, + ProtobufRequestType::XInfoConsumers => RequestType::XInfoConsumers, + ProtobufRequestType::XInfoStream => RequestType::XInfoStream, + ProtobufRequestType::Wait => RequestType::Wait, + ProtobufRequestType::XClaim => RequestType::XClaim, + ProtobufRequestType::Scan => RequestType::Scan, } } } @@ -273,8 +449,8 @@ impl RequestType { match self { RequestType::InvalidRequest => None, RequestType::CustomCommand => Some(Cmd::new()), - RequestType::GetString => Some(cmd("GET")), - RequestType::SetString => Some(cmd("SET")), + RequestType::Get => Some(cmd("GET")), + RequestType::Set => Some(cmd("SET")), RequestType::Ping => Some(cmd("PING")), RequestType::Info => Some(cmd("INFO")), RequestType::Del => Some(cmd("DEL")), @@ -298,10 +474,10 @@ impl RequestType { RequestType::ClientUnblock => Some(get_two_word_command("CLIENT", "UNBLOCK")), RequestType::ClientUnpause => Some(get_two_word_command("CLIENT", "UNPAUSE")), RequestType::Expire => Some(cmd("EXPIRE")), - RequestType::HashSet => Some(cmd("HSET")), - RequestType::HashGet => Some(cmd("HGET")), - RequestType::HashDel => Some(cmd("HDEL")), - RequestType::HashExists => Some(cmd("HEXISTS")), + RequestType::HSet => Some(cmd("HSET")), + RequestType::HGet => Some(cmd("HGET")), + RequestType::HDel => Some(cmd("HDEL")), + RequestType::HExists => Some(cmd("HEXISTS")), RequestType::MSet => Some(cmd("MSET")), RequestType::MGet => Some(cmd("MGET")), RequestType::Incr => Some(cmd("INCR")), @@ -309,11 +485,11 @@ impl RequestType { RequestType::IncrByFloat => Some(cmd("INCRBYFLOAT")), RequestType::Decr => Some(cmd("DECR")), RequestType::DecrBy => Some(cmd("DECRBY")), - RequestType::HashGetAll => Some(cmd("HGETALL")), - RequestType::HashMSet => Some(cmd("HMSET")), - RequestType::HashMGet => Some(cmd("HMGET")), - RequestType::HashIncrBy => Some(cmd("HINCRBY")), - RequestType::HashIncrByFloat => Some(cmd("HINCRBYFLOAT")), + RequestType::HGetAll => Some(cmd("HGETALL")), + RequestType::HMSet => Some(cmd("HMSET")), + RequestType::HMGet => Some(cmd("HMGET")), + RequestType::HIncrBy => Some(cmd("HINCRBY")), + RequestType::HIncrByFloat => Some(cmd("HINCRBYFLOAT")), RequestType::LPush => Some(cmd("LPUSH")), RequestType::LPop => Some(cmd("LPOP")), RequestType::RPush => Some(cmd("RPUSH")), @@ -332,11 +508,11 @@ impl RequestType { RequestType::Exists => Some(cmd("EXISTS")), RequestType::Unlink => Some(cmd("UNLINK")), RequestType::TTL => Some(cmd("TTL")), - RequestType::Zadd => Some(cmd("ZADD")), - RequestType::Zrem => Some(cmd("ZREM")), - RequestType::Zrange => Some(cmd("ZRANGE")), - RequestType::Zcard => Some(cmd("ZCARD")), - RequestType::Zcount => Some(cmd("ZCOUNT")), + RequestType::ZAdd => Some(cmd("ZADD")), + RequestType::ZRem => Some(cmd("ZREM")), + RequestType::ZRange => Some(cmd("ZRANGE")), + RequestType::ZCard => Some(cmd("ZCARD")), + RequestType::ZCount => Some(cmd("ZCOUNT")), RequestType::ZIncrBy => Some(cmd("ZINCRBY")), RequestType::ZScore => Some(cmd("ZSCORE")), RequestType::Type => Some(cmd("TYPE")), @@ -344,7 +520,7 @@ impl RequestType { RequestType::Echo => Some(cmd("ECHO")), RequestType::ZPopMin => Some(cmd("ZPOPMIN")), RequestType::Strlen => Some(cmd("STRLEN")), - RequestType::Lindex => Some(cmd("LINDEX")), + RequestType::LIndex => Some(cmd("LINDEX")), RequestType::ZPopMax => Some(cmd("ZPOPMAX")), RequestType::XAck => Some(cmd("XACK")), RequestType::XAdd => Some(cmd("XADD")), @@ -355,25 +531,25 @@ impl RequestType { RequestType::XTrim => Some(cmd("XTRIM")), RequestType::HSetNX => Some(cmd("HSETNX")), RequestType::SIsMember => Some(cmd("SISMEMBER")), - RequestType::Hvals => Some(cmd("HVALS")), + RequestType::HVals => Some(cmd("HVALS")), RequestType::PTTL => Some(cmd("PTTL")), RequestType::ZRemRangeByRank => Some(cmd("ZREMRANGEBYRANK")), RequestType::Persist => Some(cmd("PERSIST")), RequestType::ZRemRangeByScore => Some(cmd("ZREMRANGEBYSCORE")), RequestType::Time => Some(cmd("TIME")), - RequestType::Zrank => Some(cmd("ZRANK")), + RequestType::ZRank => Some(cmd("ZRANK")), RequestType::Rename => Some(cmd("RENAME")), RequestType::DBSize => Some(cmd("DBSIZE")), - RequestType::Brpop => Some(cmd("BRPOP")), - RequestType::Hkeys => Some(cmd("HKEYS")), + RequestType::BRPop => Some(cmd("BRPOP")), + RequestType::HKeys => Some(cmd("HKEYS")), RequestType::PfAdd => Some(cmd("PFADD")), RequestType::PfCount => Some(cmd("PFCOUNT")), RequestType::PfMerge => Some(cmd("PFMERGE")), RequestType::RPushX => Some(cmd("RPUSHX")), RequestType::LPushX => Some(cmd("LPUSHX")), - RequestType::Blpop => Some(cmd("BLPOP")), + RequestType::BLPop => Some(cmd("BLPOP")), RequestType::LInsert => Some(cmd("LINSERT")), - RequestType::Spop => Some(cmd("SPOP")), + RequestType::SPop => Some(cmd("SPOP")), RequestType::ZMScore => Some(cmd("ZMSCORE")), RequestType::ZDiff => Some(cmd("ZDIFF")), RequestType::ZDiffStore => Some(cmd("ZDIFFSTORE")), @@ -389,9 +565,99 @@ impl RequestType { RequestType::GetRange => Some(cmd("GETRANGE")), RequestType::SMove => Some(cmd("SMOVE")), RequestType::SMIsMember => Some(cmd("SMISMEMBER")), + RequestType::ZUnionStore => Some(cmd("ZUNIONSTORE")), RequestType::LastSave => Some(cmd("LASTSAVE")), RequestType::GeoAdd => Some(cmd("GEOADD")), RequestType::GeoHash => Some(cmd("GEOHASH")), + RequestType::ObjectEncoding => Some(get_two_word_command("OBJECT", "ENCODING")), + RequestType::ObjectFreq => Some(get_two_word_command("OBJECT", "FREQ")), + RequestType::ObjectIdleTime => Some(get_two_word_command("OBJECT", "IDLETIME")), + RequestType::GeoDist => Some(cmd("GEODIST")), + RequestType::SDiff => Some(cmd("SDIFF")), + RequestType::ObjectRefCount => Some(get_two_word_command("OBJECT", "REFCOUNT")), + RequestType::Lolwut => Some(cmd("LOLWUT")), + RequestType::GeoPos => Some(cmd("GEOPOS")), + RequestType::BZPopMax => Some(cmd("BZPOPMAX")), + RequestType::RenameNX => Some(cmd("RENAMENX")), + RequestType::Touch => Some(cmd("TOUCH")), + RequestType::ZRevRank => Some(cmd("ZREVRANK")), + RequestType::ZInterStore => Some(cmd("ZINTERSTORE")), + RequestType::HRandField => Some(cmd("HRANDFIELD")), + RequestType::ZUnion => Some(cmd("ZUNION")), + RequestType::BZPopMin => Some(cmd("BZPOPMIN")), + RequestType::FlushAll => Some(cmd("FLUSHALL")), + RequestType::ZRandMember => Some(cmd("ZRANDMEMBER")), + RequestType::BitCount => Some(cmd("BITCOUNT")), + RequestType::BZMPop => Some(cmd("BZMPOP")), + RequestType::LMPop => Some(cmd("LMPOP")), + RequestType::BLMPop => Some(cmd("BLMPOP")), + RequestType::SetBit => Some(cmd("SETBIT")), + RequestType::ZInterCard => Some(cmd("ZINTERCARD")), + RequestType::ZMPop => Some(cmd("ZMPOP")), + RequestType::GetBit => Some(cmd("GETBIT")), + RequestType::ZInter => Some(cmd("ZINTER")), + RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")), + RequestType::FunctionList => Some(get_two_word_command("FUNCTION", "LIST")), + RequestType::FunctionDelete => Some(get_two_word_command("FUNCTION", "DELETE")), + RequestType::FunctionFlush => Some(get_two_word_command("FUNCTION", "FLUSH")), + RequestType::FCall => Some(cmd("FCALL")), + RequestType::BitPos => Some(cmd("BITPOS")), + RequestType::BitOp => Some(cmd("BITOP")), + RequestType::HStrlen => Some(cmd("HSTRLEN")), + RequestType::ExpireTime => Some(cmd("EXPIRETIME")), + RequestType::PExpireTime => Some(cmd("PEXPIRETIME")), + RequestType::XLen => Some(cmd("XLEN")), + RequestType::FunctionKill => Some(get_two_word_command("FUNCTION", "KILL")), + RequestType::FunctionStats => Some(get_two_word_command("FUNCTION", "STATS")), + RequestType::FCallReadOnly => Some(cmd("FCALL_RO")), + RequestType::FlushDB => Some(cmd("FLUSHDB")), + RequestType::LSet => Some(cmd("LSET")), + RequestType::XDel => Some(cmd("XDEL")), + RequestType::XRange => Some(cmd("XRANGE")), + RequestType::LMove => Some(cmd("LMOVE")), + RequestType::BLMove => Some(cmd("BLMOVE")), + RequestType::GetDel => Some(cmd("GETDEL")), + RequestType::SRandMember => Some(cmd("SRANDMEMBER")), + RequestType::BitField => Some(cmd("BITFIELD")), + RequestType::BitFieldReadOnly => Some(cmd("BITFIELD_RO")), + RequestType::Move => Some(cmd("MOVE")), + RequestType::SInterCard => Some(cmd("SINTERCARD")), + RequestType::Copy => Some(cmd("COPY")), + RequestType::Sort => Some(cmd("SORT")), + RequestType::XRevRange => Some(cmd("XREVRANGE")), + RequestType::MSetNX => Some(cmd("MSETNX")), + RequestType::LPos => Some(cmd("LPOS")), + RequestType::LCS => Some(cmd("LCS")), + RequestType::GeoSearch => Some(cmd("GEOSEARCH")), + RequestType::SUnion => Some(cmd("SUNION")), + RequestType::Watch => Some(cmd("WATCH")), + RequestType::UnWatch => Some(cmd("UNWATCH")), + RequestType::GeoSearchStore => Some(cmd("GEOSEARCHSTORE")), + RequestType::Publish => Some(cmd("PUBLISH")), + RequestType::SPublish => Some(cmd("SPUBLISH")), + RequestType::XGroupCreateConsumer => { + Some(get_two_word_command("XGROUP", "CREATECONSUMER")) + } + RequestType::XGroupDelConsumer => Some(get_two_word_command("XGROUP", "DELCONSUMER")), + RequestType::RandomKey => Some(cmd("RANDOMKEY")), + RequestType::GetEx => Some(cmd("GETEX")), + RequestType::Dump => Some(cmd("DUMP")), + RequestType::Restore => Some(cmd("RESTORE")), + RequestType::SortReadOnly => Some(cmd("SORT_RO")), + RequestType::FunctionDump => Some(get_two_word_command("FUNCTION", "DUMP")), + RequestType::FunctionRestore => Some(get_two_word_command("FUNCTION", "RESTORE")), + RequestType::XPending => Some(cmd("XPENDING")), + RequestType::XGroupSetId => Some(get_two_word_command("XGROUP", "SETID")), + RequestType::SScan => Some(cmd("SSCAN")), + RequestType::ZScan => Some(cmd("ZSCAN")), + RequestType::HScan => Some(cmd("HSCAN")), + RequestType::XAutoClaim => Some(cmd("XAUTOCLAIM")), + RequestType::XInfoGroups => Some(get_two_word_command("XINFO", "GROUPS")), + RequestType::XInfoConsumers => Some(get_two_word_command("XINFO", "CONSUMERS")), + RequestType::XInfoStream => Some(get_two_word_command("XINFO", "STREAM")), + RequestType::Wait => Some(cmd("WAIT")), + RequestType::XClaim => Some(cmd("XCLAIM")), + RequestType::Scan => Some(cmd("SCAN")), } } } diff --git a/glide-core/src/retry_strategies.rs b/glide-core/src/retry_strategies.rs index 4dd5d7edb7..dbe5683347 100644 --- a/glide-core/src/retry_strategies.rs +++ b/glide-core/src/retry_strategies.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use crate::client::ConnectionRetryStrategy; use std::time::Duration; diff --git a/glide-core/src/rotating_buffer.rs b/glide-core/src/rotating_buffer.rs index bbc736162e..b87f666605 100644 --- a/glide-core/src/rotating_buffer.rs +++ b/glide-core/src/rotating_buffer.rs @@ -1,7 +1,8 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -use bytes::BytesMut; +#[allow(unused_imports)] +use bytes::{Bytes, BytesMut}; use integer_encoding::VarInt; use logger_core::log_error; use protobuf::Message; @@ -64,8 +65,8 @@ impl RotatingBuffer { #[cfg(test)] mod tests { use super::*; - use crate::redis_request::{command, redis_request}; - use crate::redis_request::{Command, RedisRequest, RequestType}; + use crate::command_request::{command, command_request}; + use crate::command_request::{Command, CommandRequest, RequestType}; use bytes::BufMut; use rand::{distributions::Alphanumeric, Rng}; use rstest::rstest; @@ -79,31 +80,31 @@ mod tests { fn create_command_request( callback_index: u32, - args: Vec, + args: Vec, request_type: RequestType, args_pointer: bool, - ) -> RedisRequest { - let mut request = RedisRequest::new(); + ) -> CommandRequest { + let mut request = CommandRequest::new(); request.callback_idx = callback_index; let mut command = Command::new(); command.request_type = request_type.into(); if args_pointer { command.args = Some(command::Args::ArgsVecPointer(Box::leak(Box::new(args)) - as *mut Vec + as *mut Vec as u64)); } else { let mut args_array = command::ArgsArray::new(); - args_array.args = args.into_iter().map(|str| str.into()).collect(); + args_array.args.clone_from(&args); command.args = Some(command::Args::ArgsArray(args_array)); } - request.command = Some(redis_request::Command::SingleCommand(command)); + request.command = Some(command_request::Command::SingleCommand(command)); request } fn write_message( buffer: &mut BytesMut, callback_index: u32, - args: Vec, + args: Vec, request_type: RequestType, args_pointer: bool, ) { @@ -117,8 +118,8 @@ mod tests { write_message( buffer, callback_index, - vec![key.to_string()], - RequestType::GetString, + vec![Bytes::from(key.to_string())], + RequestType::Get, args_pointer, ); } @@ -127,39 +128,34 @@ mod tests { buffer: &mut BytesMut, callback_index: u32, key: &str, - value: String, + value: Bytes, args_pointer: bool, ) { write_message( buffer, callback_index, - vec![key.to_string(), value], - RequestType::SetString, + vec![Bytes::from(key.to_string()), value], + RequestType::Set, args_pointer, ); } fn assert_request( - request: &RedisRequest, + request: &CommandRequest, expected_type: RequestType, expected_index: u32, - expected_args: Vec, + expected_args: Vec, args_pointer: bool, ) { assert_eq!(request.callback_idx, expected_index); - let Some(redis_request::Command::SingleCommand(ref command)) = request.command else { + let Some(command_request::Command::SingleCommand(ref command)) = request.command else { panic!("expected single command"); }; assert_eq!(command.request_type, expected_type.into()); - let args: Vec = if args_pointer { - *unsafe { Box::from_raw(command.args_vec_pointer() as *mut Vec) } + let args: Vec = if args_pointer { + *unsafe { Box::from_raw(command.args_vec_pointer() as *mut Vec) } } else { - command - .args_array() - .args - .iter() - .map(|chars| chars.to_string()) - .collect() + command.args_array().args.to_vec() }; assert_eq!(args, expected_args); } @@ -188,23 +184,23 @@ mod tests { rotating_buffer.current_buffer(), 5, "key", - "value".to_string(), + "value".into(), args_pointer, ); let requests = rotating_buffer.get_requests().unwrap(); assert_eq!(requests.len(), 2); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec!["key".to_string()], + vec!["key".into()], args_pointer, ); assert_request( &requests[1], - RequestType::SetString, + RequestType::Set, 5, - vec!["key".to_string(), "value".to_string()], + vec!["key".into(), "value".into()], args_pointer, ); } @@ -217,25 +213,25 @@ mod tests { let requests = rotating_buffer.get_requests().unwrap(); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec!["key".to_string()], + vec!["key".into()], args_pointer, ); write_set( rotating_buffer.current_buffer(), 5, "key", - "value".to_string(), + "value".into(), args_pointer, ); let requests = rotating_buffer.get_requests().unwrap(); assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::SetString, + RequestType::Set, 5, - vec!["key".to_string(), "value".to_string()], + vec!["key".into(), "value".into()], args_pointer, ); } @@ -250,9 +246,9 @@ mod tests { assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec!["key".to_string()], + vec!["key".into()], false, ); @@ -261,9 +257,9 @@ mod tests { } assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec!["key".to_string()], + vec!["key".into()], false, ); } @@ -284,9 +280,9 @@ mod tests { assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec!["key1".to_string()], + vec!["key1".into()], args_pointer, ); let buffer = rotating_buffer.current_buffer(); @@ -296,9 +292,9 @@ mod tests { assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 101, - vec!["key2".to_string()], + vec!["key2".into()], args_pointer, ); } @@ -316,7 +312,7 @@ mod tests { let required_varint_length = u32::required_space(KEY_LENGTH as u32); assert!(required_varint_length > 1); // so we could split the write of the varint buffer.extend_from_slice(&request_bytes[..NUM_OF_LENGTH_BYTES]); - let requests = rotating_buffer.get_requests::().unwrap(); + let requests = rotating_buffer.get_requests::().unwrap(); assert_eq!(requests.len(), 0); let buffer = rotating_buffer.current_buffer(); buffer.extend_from_slice(&request_bytes[NUM_OF_LENGTH_BYTES..]); @@ -324,9 +320,9 @@ mod tests { assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec![key], + vec![key.into()], args_pointer, ); } @@ -351,9 +347,9 @@ mod tests { assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 100, - vec!["key1".to_string()], + vec!["key1".into()], args_pointer, ); let buffer = rotating_buffer.current_buffer(); @@ -363,9 +359,9 @@ mod tests { assert_eq!(requests.len(), 1); assert_request( &requests[0], - RequestType::GetString, + RequestType::Get, 101, - vec![key2], + vec![key2.into()], args_pointer, ); } diff --git a/glide-core/src/scripts_container.rs b/glide-core/src/scripts_container.rs index 251a69e5c3..a039593f79 100644 --- a/glide-core/src/scripts_container.rs +++ b/glide-core/src/scripts_container.rs @@ -1,17 +1,19 @@ +use bytes::BytesMut; /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -use arcstr::ArcStr; use logger_core::log_info; use once_cell::sync::Lazy; use sha1_smol::Sha1; -use std::{collections::HashMap, sync::Mutex}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; -static CONTAINER: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +static CONTAINER: Lazy>>> = + Lazy::new(|| Mutex::new(HashMap::new())); -pub fn add_script(script: &str) -> String { +pub fn add_script(script: &[u8]) -> String { let mut hash = Sha1::new(); - hash.update(script.as_bytes()); + hash.update(script); let hash = hash.digest().to_string(); log_info( "script lifetime", @@ -20,11 +22,11 @@ pub fn add_script(script: &str) -> String { CONTAINER .lock() .unwrap() - .insert(hash.clone(), script.into()); + .insert(hash.clone(), Arc::new(script.into())); hash } -pub fn get_script(hash: &str) -> Option { +pub fn get_script(hash: &str) -> Option> { CONTAINER.lock().unwrap().get(hash).cloned() } diff --git a/glide-core/src/socket_listener.rs b/glide-core/src/socket_listener.rs index fc72b49a46..c799b4a576 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -1,26 +1,27 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use super::rotating_buffer::RotatingBuffer; use crate::client::Client; +use crate::cluster_scan_container::get_cluster_scan_cursor; +use crate::command_request::{ + command, command_request, ClusterScan, Command, CommandRequest, Routes, SlotTypes, Transaction, +}; use crate::connection_request::ConnectionRequest; use crate::errors::{error_message, error_type, RequestErrorType}; -use crate::redis_request::{ - command, redis_request, Command, RedisRequest, Routes, ScriptInvocation, SlotTypes, Transaction, -}; use crate::response; use crate::response::Response; use crate::retry_strategies::get_fixed_interval_backoff; +use bytes::Bytes; use directories::BaseDirs; use dispose::{Disposable, Dispose}; use logger_core::{log_debug, log_error, log_info, log_trace, log_warn}; -use protobuf::Message; +use protobuf::{Chars, Message}; use redis::cluster_routing::{ MultipleNodeRoutingInfo, Route, RoutingInfo, SingleNodeRoutingInfo, SlotAddr, }; use redis::cluster_routing::{ResponsePolicy, Routable}; -use redis::RedisError; -use redis::{Cmd, Value}; +use redis::{Cmd, PushInfo, RedisError, ScanStateRC, Value}; use std::cell::Cell; use std::rc::Rc; use std::{env, str}; @@ -29,6 +30,7 @@ use thiserror::Error; use tokio::io::ErrorKind::AddrInUse; use tokio::net::{UnixListener, UnixStream}; use tokio::runtime::Builder; +use tokio::sync::mpsc; use tokio::sync::mpsc::{channel, Sender}; use tokio::sync::Mutex; use tokio::task; @@ -44,6 +46,13 @@ const SOCKET_FILE_NAME: &str = "glide-socket"; /// strings instead of a pointer pub const MAX_REQUEST_ARGS_LENGTH: usize = 2_i32.pow(12) as usize; // TODO: find the right number +pub const STRING: &str = "string"; +pub const LIST: &str = "list"; +pub const SET: &str = "set"; +pub const ZSET: &str = "zset"; +pub const HASH: &str = "hash"; +pub const STREAM: &str = "stream"; + /// struct containing all objects needed to bind to a socket and clean it. struct SocketListener { socket_path: String, @@ -108,9 +117,15 @@ impl UnixStreamListener { return ReadSocketClosed.into(); } Ok(_) => { - return match self.rotating_buffer.get_requests() { - Ok(requests) => ReceivedValues(requests), - Err(err) => UnhandledError(err.into()).into(), + match self.rotating_buffer.get_requests() { + Ok(requests) => { + if !requests.is_empty() { + return ReceivedValues(requests); + } + // continue to read from socket + continue; + } + Err(err) => return UnhandledError(err.into()).into(), }; } Err(ref e) @@ -183,6 +198,7 @@ async fn write_result( ) -> Result<(), io::Error> { let mut response = Response::new(); response.callback_idx = callback_index; + response.is_push = false; response.value = match resp_result { Ok(Value::Okay) => Some(response::response::Value::ConstantResponse( response::ConstantResponse::OK.into(), @@ -198,13 +214,13 @@ async fn write_result( None } } - Err(ClienUsageError::Internal(error_message)) => { + Err(ClientUsageError::Internal(error_message)) => { log_error("internal error", &error_message); Some(response::response::Value::ClosingError( error_message.into(), )) } - Err(ClienUsageError::User(error_message)) => { + Err(ClientUsageError::User(error_message)) => { log_error("user error", &error_message); let request_error = response::RequestError { type_: response::RequestErrorType::Unspecified.into(), @@ -213,7 +229,7 @@ async fn write_result( }; Some(response::response::Value::RequestError(request_error)) } - Err(ClienUsageError::Redis(err)) => { + Err(ClientUsageError::Redis(err)) => { let error_message = error_message(&err); log_warn("received error", error_message.as_str()); log_debug("received error", format!("for callback {}", callback_index)); @@ -261,9 +277,9 @@ fn get_command(request: &Command) -> Option { request_type.get_command() } -fn get_redis_command(command: &Command) -> Result { +fn get_redis_command(command: &Command) -> Result { let Some(mut cmd) = get_command(command) else { - return Err(ClienUsageError::Internal(format!( + return Err(ClientUsageError::Internal(format!( "Received invalid request type: {:?}", command.request_type ))); @@ -272,24 +288,24 @@ fn get_redis_command(command: &Command) -> Result { match &command.args { Some(command::Args::ArgsArray(args_vec)) => { for arg in args_vec.args.iter() { - cmd.arg(arg.as_bytes()); + cmd.arg(arg.as_ref()); } } Some(command::Args::ArgsVecPointer(pointer)) => { - let res = *unsafe { Box::from_raw(*pointer as *mut Vec) }; + let res = *unsafe { Box::from_raw(*pointer as *mut Vec) }; for arg in res { - cmd.arg(arg.as_bytes()); + cmd.arg(arg.as_ref()); } } None => { - return Err(ClienUsageError::Internal( + return Err(ClientUsageError::Internal( "Failed to get request arguments, no arguments are set".to_string(), )); } }; if cmd.args_iter().next().is_none() { - return Err(ClienUsageError::User( + return Err(ClientUsageError::User( "Received command without a command name or arguments".into(), )); } @@ -308,13 +324,66 @@ async fn send_command( .map_err(|err| err.into()) } +// Parse the cluster scan command parameters from protobuf and send the command to redis-rs. +async fn cluster_scan(cluster_scan: ClusterScan, mut client: Client) -> ClientUsageResult { + // Since we don't send the cluster scan as a usual command, but through a special function in redis-rs library, + // we need to handle the command separately. + // Specifically, we need to handle the cursor, which is not the cursor returned from the server, + // but the ID of the ScanStateRC, stored in the cluster scan container. + // We need to get the ref from the table or create a new one if the cursor is empty. + let cursor: String = cluster_scan.cursor.into(); + let cluster_scan_cursor = if cursor.is_empty() { + ScanStateRC::new() + } else { + get_cluster_scan_cursor(cursor)? + }; + + let match_pattern = cluster_scan.match_pattern.map(|pattern| pattern.into()); + let count = cluster_scan.count.map(|count| count as usize); + + let object_type = match cluster_scan.object_type { + Some(char_object_type) => match char_object_type.to_string().to_lowercase().as_str() { + STRING => Some(redis::ObjectType::String), + LIST => Some(redis::ObjectType::List), + SET => Some(redis::ObjectType::Set), + ZSET => Some(redis::ObjectType::ZSet), + HASH => Some(redis::ObjectType::Hash), + STREAM => Some(redis::ObjectType::Stream), + _ => { + return Err(ClientUsageError::Internal(format!( + "Received invalid object type: {:?}", + char_object_type + ))) + } + }, + None => None, + }; + + client + .cluster_scan(&cluster_scan_cursor, &match_pattern, count, object_type) + .await + .map_err(|err| err.into()) +} + async fn invoke_script( - script: ScriptInvocation, + hash: Chars, + keys: Option>, + args: Option>, mut client: Client, routing: Option, ) -> ClientUsageResult { + // convert Vec to vec<[u8]> + let keys: Vec<&[u8]> = keys + .as_ref() + .map(|keys| keys.iter().map(|e| e.as_ref()).collect()) + .unwrap_or_default(); + let args: Vec<&[u8]> = args + .as_ref() + .map(|keys| keys.iter().map(|e| e.as_ref()).collect()) + .unwrap_or_default(); + client - .invoke_script(&script.hash, script.keys, script.args, routing) + .invoke_script(&hash, &keys, &args, routing) .await .map_err(|err| err.into()) } @@ -343,14 +412,14 @@ fn get_slot_addr(slot_type: &protobuf::EnumOrUnknown) -> ClientUsageR SlotTypes::Primary => SlotAddr::Master, SlotTypes::Replica => SlotAddr::ReplicaRequired, }) - .map_err(|id| ClienUsageError::Internal(format!("Received unexpected slot id type {id}"))) + .map_err(|id| ClientUsageError::Internal(format!("Received unexpected slot id type {id}"))) } fn get_route( route: Option>, cmd: Option<&Cmd>, ) -> ClientUsageResult> { - use crate::redis_request::routes::Value; + use crate::command_request::routes::Value; let Some(route) = route.and_then(|route| route.value) else { return Ok(None); }; @@ -363,20 +432,19 @@ fn get_route( match route { Value::SimpleRoutes(simple_route) => { let simple_route = simple_route.enum_value().map_err(|id| { - ClienUsageError::Internal(format!("Received unexpected simple route type {id}")) + ClientUsageError::Internal(format!("Received unexpected simple route type {id}")) })?; match simple_route { - crate::redis_request::SimpleRoutes::AllNodes => Ok(Some(RoutingInfo::MultiNode(( - MultipleNodeRoutingInfo::AllNodes, - get_response_policy(cmd), - )))), - crate::redis_request::SimpleRoutes::AllPrimaries => { + crate::command_request::SimpleRoutes::AllNodes => Ok(Some(RoutingInfo::MultiNode( + (MultipleNodeRoutingInfo::AllNodes, get_response_policy(cmd)), + ))), + crate::command_request::SimpleRoutes::AllPrimaries => { Ok(Some(RoutingInfo::MultiNode(( MultipleNodeRoutingInfo::AllMasters, get_response_policy(cmd), )))) } - crate::redis_request::SimpleRoutes::Random => { + crate::command_request::SimpleRoutes::Random => { Ok(Some(RoutingInfo::SingleNode(SingleNodeRoutingInfo::Random))) } } @@ -408,11 +476,14 @@ fn get_route( } } -fn handle_request(request: RedisRequest, client: Client, writer: Rc) { +fn handle_request(request: CommandRequest, client: Client, writer: Rc) { task::spawn_local(async move { let result = match request.command { Some(action) => match action { - redis_request::Command::SingleCommand(command) => { + command_request::Command::ClusterScan(cluster_scan_command) => { + cluster_scan(cluster_scan_command, client).await + } + command_request::Command::SingleCommand(command) => { match get_redis_command(&command) { Ok(cmd) => match get_route(request.route.0, Some(&cmd)) { Ok(routes) => send_command(cmd, client, routes).await, @@ -421,15 +492,36 @@ fn handle_request(request: RedisRequest, client: Client, writer: Rc) { Err(e) => Err(e), } } - redis_request::Command::Transaction(transaction) => { + command_request::Command::Transaction(transaction) => { match get_route(request.route.0, None) { Ok(routes) => send_transaction(transaction, client, routes).await, Err(e) => Err(e), } } - redis_request::Command::ScriptInvocation(script) => { + command_request::Command::ScriptInvocation(script) => { match get_route(request.route.0, None) { - Ok(routes) => invoke_script(script, client, routes).await, + Ok(routes) => { + invoke_script( + script.hash, + Some(script.keys), + Some(script.args), + client, + routes, + ) + .await + } + Err(e) => Err(e), + } + } + command_request::Command::ScriptInvocationPointers(script) => { + let keys = script + .keys_pointer + .map(|pointer| *unsafe { Box::from_raw(pointer as *mut Vec) }); + let args = script + .args_pointer + .map(|pointer| *unsafe { Box::from_raw(pointer as *mut Vec) }); + match get_route(request.route.0, None) { + Ok(routes) => invoke_script(script.hash, keys, args, client, routes).await, Err(e) => Err(e), } } @@ -442,7 +534,7 @@ fn handle_request(request: RedisRequest, client: Client, writer: Rc) { request.callback_idx ), ); - Err(ClienUsageError::Internal( + Err(ClientUsageError::Internal( "Received empty request".to_string(), )) } @@ -453,7 +545,7 @@ fn handle_request(request: RedisRequest, client: Client, writer: Rc) { } async fn handle_requests( - received_requests: Vec, + received_requests: Vec, client: &Client, writer: &Rc, ) { @@ -472,8 +564,9 @@ pub fn close_socket(socket_path: &String) { async fn create_client( writer: &Rc, request: ConnectionRequest, + push_tx: Option>, ) -> Result { - let client = match Client::new(request.into()).await { + let client = match Client::new(request.into(), push_tx).await { Ok(client) => client, Err(err) => return Err(ClientCreationError::ConnectionError(err)), }; @@ -484,13 +577,14 @@ async fn create_client( async fn wait_for_connection_configuration_and_create_client( client_listener: &mut UnixStreamListener, writer: &Rc, + push_tx: Option>, ) -> Result { // Wait for the server's address match client_listener.next_values::().await { Closed(reason) => Err(ClientCreationError::SocketListenerClosed(reason)), ReceivedValues(mut received_requests) => { if let Some(request) = received_requests.pop() { - create_client(writer, request).await + create_client(writer, request, push_tx).await } else { Err(ClientCreationError::UnhandledError( "No received requests".to_string(), @@ -517,6 +611,35 @@ async fn read_values_loop( } } +async fn push_manager_loop(mut push_rx: mpsc::UnboundedReceiver, writer: Rc) { + loop { + let result = push_rx.recv().await; + match result { + None => { + log_error("push manager loop", "got None from push manager"); + return; + } + Some(push_msg) => { + log_debug("push manager loop", format!("got PushInfo: {:?}", push_msg)); + let mut response = Response::new(); + response.callback_idx = 0; // callback_idx is not used with push notifications + response.is_push = true; + response.value = { + let push_val = Value::Push { + kind: (push_msg.kind), + data: (push_msg.data), + }; + let pointer = Box::leak(Box::new(push_val)); + let raw_pointer = pointer as *mut redis::Value; + Some(response::response::Value::RespPointer(raw_pointer as u64)) + }; + + _ = write_to_writer(response, &writer).await; + } + } + } +} + async fn listen_on_client_stream(socket: UnixStream) { let socket = Rc::new(socket); // Spawn a new task to listen on this client's stream @@ -524,14 +647,18 @@ async fn listen_on_client_stream(socket: UnixStream) { let mut client_listener = UnixStreamListener::new(socket.clone()); let accumulated_outputs = Cell::new(Vec::new()); let (sender, mut receiver) = channel(1); + let (push_tx, push_rx) = tokio::sync::mpsc::unbounded_channel(); let writer = Rc::new(Writer { socket, lock: write_lock, accumulated_outputs, closing_sender: sender, }); - let client_creation = - wait_for_connection_configuration_and_create_client(&mut client_listener, &writer); + let client_creation = wait_for_connection_configuration_and_create_client( + &mut client_listener, + &writer, + Some(push_tx), + ); let client = match client_creation.await { Ok(conn) => conn, Err(ClientCreationError::SocketListenerClosed(ClosingReason::ReadSocketClosed)) => { @@ -582,6 +709,9 @@ async fn listen_on_client_stream(socket: UnixStream) { } else { log_trace("client closing", "writer closed"); } + }, + _ = push_manager_loop(push_rx, writer.clone()) => { + log_trace("client closing", "push manager closed"); } } log_trace("client closing", "closing connection"); @@ -722,7 +852,7 @@ enum ClientCreationError { /// Enum describing errors received during client usage. #[derive(Debug, Error)] -enum ClienUsageError { +enum ClientUsageError { #[error("Redis error: {0}")] Redis(#[from] RedisError), /// An error that stems from wrong behavior of the client. @@ -733,7 +863,7 @@ enum ClienUsageError { User(String), } -type ClientUsageResult = Result; +type ClientUsageResult = Result; /// Defines errors caused the connection to close. #[derive(Debug, Clone)] diff --git a/glide-core/tests/test_client.rs b/glide-core/tests/test_client.rs index 945c9db504..2dfe9fc248 100644 --- a/glide-core/tests/test_client.rs +++ b/glide-core/tests/test_client.rs @@ -1,12 +1,12 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ mod utilities; #[cfg(test)] pub(crate) mod shared_client_tests { use super::*; - use glide_core::client::Client; + use glide_core::client::{Client, DEFAULT_RESPONSE_TIMEOUT}; use redis::{ cluster_routing::{MultipleNodeRoutingInfo, RoutingInfo}, FromRedisValue, InfoDict, RedisConnectionInfo, Value, @@ -21,6 +21,31 @@ pub(crate) mod shared_client_tests { client: Client, } + async fn create_client(server: &BackingServer, configuration: TestConfiguration) -> Client { + match server { + BackingServer::Standalone(server) => { + let connection_addr = server + .as_ref() + .map(|server| server.get_client_addr()) + .unwrap_or(get_shared_server_address(configuration.use_tls)); + + // TODO - this is a patch, handling the situation where the new server + // still isn't available to connection. This should be fixed in [RedisServer]. + repeat_try_create(|| async { + Client::new( + create_connection_request(&[connection_addr.clone()], &configuration) + .into(), + None, + ) + .await + .ok() + }) + .await + } + BackingServer::Cluster(cluster) => create_cluster_client(cluster, configuration).await, + } + } + async fn setup_test_basics(use_cluster: bool, configuration: TestConfiguration) -> TestBasics { if use_cluster { let cluster_basics = cluster::setup_test_basics_internal(configuration).await; @@ -30,32 +55,14 @@ pub(crate) mod shared_client_tests { } } else { let test_basics = utilities::setup_test_basics_internal(&configuration).await; - - let connection_addr = test_basics - .server - .as_ref() - .map(|server| server.get_client_addr()) - .unwrap_or(get_shared_server_address(configuration.use_tls)); - - // TODO - this is a patch, handling the situation where the new server - // still isn't available to connection. This should be fixed in [RedisServer]. - let client = repeat_try_create(|| async { - Client::new( - create_connection_request(&[connection_addr.clone()], &configuration).into(), - ) - .await - .ok() - }) - .await; - - TestBasics { - server: BackingServer::Standalone(test_basics.server), - client, - } + let server = BackingServer::Standalone(test_basics.server); + let client = create_client(&server, configuration).await; + TestBasics { server, client } } } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_send_set_and_get( #[values(false, true)] use_tls: bool, @@ -77,6 +84,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_pipeline_is_not_routed() { // This test checks that a transaction without user routing isn't routed to a random node before reaching its target. @@ -159,6 +167,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_resp_support(#[values(false, true)] use_cluster: bool, #[values(2, 3)] protocol: i64) { let protocol_enum = match protocol { @@ -217,6 +226,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_client_handle_concurrent_workload_without_dropping_or_changing_values( #[values(false, true)] use_tls: bool, @@ -246,6 +256,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_report_closing_when_server_closes(#[values(false, true)] use_cluster: bool) { block_on_all(async { @@ -271,6 +282,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_authenticate_with_password(#[values(false, true)] use_cluster: bool) { block_on_all(async { @@ -292,6 +304,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_authenticate_with_password_and_username(#[values(false, true)] use_cluster: bool) { block_on_all(async { @@ -314,13 +327,52 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_request_timeout(#[values(false, true)] use_cluster: bool) { block_on_all(async { let mut test_basics = setup_test_basics( use_cluster, TestConfiguration { - request_timeout: Some(1), + request_timeout: Some(1), // milliseconds + shared_server: false, + ..Default::default() + }, + ) + .await; + let mut cmd = redis::Cmd::new(); + // Create a long running command to ensure we get into timeout + cmd.arg("EVAL") + .arg( + r#" + while (true) + do + redis.call('ping') + end + "#, + ) + .arg("0"); + let result = test_basics.client.send_command(&cmd, None).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.is_timeout(), "{err}"); + }); + } + + #[rstest] + #[serial_test::serial] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + fn test_blocking_command_doesnt_raise_timeout_error(#[values(false, true)] use_cluster: bool) { + // We test that the request timeout is based on the value specified in the blocking command argument, + // and not on the one set in the client configuration. To achieve this, we execute a command designed to + // be blocked until it reaches the specified command timeout. We set the client's request timeout to + // a shorter duration than the blocking command's timeout. Subsequently, we confirm that we receive + // a response from the server instead of encountering a timeout error. + block_on_all(async { + let mut test_basics = setup_test_basics( + use_cluster, + TestConfiguration { + request_timeout: Some(1), // milliseconds shared_server: true, ..Default::default() }, @@ -328,15 +380,69 @@ pub(crate) mod shared_client_tests { .await; let mut cmd = redis::Cmd::new(); - cmd.arg("BLPOP").arg("foo").arg(0); // 0 timeout blocks indefinitely + cmd.arg("BLPOP").arg(generate_random_string(10)).arg(0.3); // server should return null after 300 millisecond + let result = test_basics.client.send_command(&cmd, None).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Value::Nil); + }); + } + + #[rstest] + #[serial_test::serial] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + fn test_blocking_command_with_negative_timeout_returns_error( + #[values(false, true)] use_cluster: bool, + ) { + // We test that when blocking command is passed with a negative timeout the command will return with an error + block_on_all(async { + let mut test_basics = setup_test_basics( + use_cluster, + TestConfiguration { + request_timeout: Some(1), // milliseconds + shared_server: true, + ..Default::default() + }, + ) + .await; + let mut cmd = redis::Cmd::new(); + cmd.arg("BLPOP").arg(generate_random_string(10)).arg(-1); let result = test_basics.client.send_command(&cmd, None).await; assert!(result.is_err()); let err = result.unwrap_err(); - assert!(err.is_timeout(), "{err}"); + assert_eq!(err.kind(), redis::ErrorKind::ResponseError); + assert!(err.to_string().contains("negative")); + }); + } + + #[rstest] + #[serial_test::serial] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + fn test_blocking_command_with_zero_timeout_blocks_indefinitely( + #[values(false, true)] use_cluster: bool, + ) { + // We test that when a blocking command is passed with a timeout duration of 0, it will block the client indefinitely + block_on_all(async { + let config = TestConfiguration { + request_timeout: Some(1), // millisecond + shared_server: true, + ..Default::default() + }; + let mut test_basics = setup_test_basics(use_cluster, config).await; + let key = generate_random_string(10); + let future = async move { + let mut cmd = redis::Cmd::new(); + cmd.arg("BLPOP").arg(key).arg(0); // `0` should block indefinitely + test_basics.client.send_command(&cmd, None).await + }; + // We execute the command with Tokio's timeout wrapper to prevent the test from hanging indefinitely. + let tokio_timeout_result = + tokio::time::timeout(DEFAULT_RESPONSE_TIMEOUT * 2, future).await; + assert!(tokio_timeout_result.is_err()); }); } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_request_transaction_timeout(#[values(false, true)] use_cluster: bool) { block_on_all(async { @@ -375,6 +481,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_client_name_after_reconnection(#[values(false, true)] use_cluster: bool) { const CLIENT_NAME: &str = "TEST_CLIENT_NAME"; @@ -424,6 +531,7 @@ pub(crate) mod shared_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_request_transaction_and_convert_all_values(#[values(false, true)] use_cluster: bool) { block_on_all(async { diff --git a/glide-core/tests/test_cluster_client.rs b/glide-core/tests/test_cluster_client.rs index de3e22e15a..1c60dc8c79 100644 --- a/glide-core/tests/test_cluster_client.rs +++ b/glide-core/tests/test_cluster_client.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ mod utilities; diff --git a/glide-core/tests/test_socket_listener.rs b/glide-core/tests/test_socket_listener.rs index 816d0e2992..d538a28a14 100644 --- a/glide-core/tests/test_socket_listener.rs +++ b/glide-core/tests/test_socket_listener.rs @@ -1,5 +1,5 @@ /* - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ #![cfg(feature = "socket-layer")] @@ -22,13 +22,13 @@ mod socket_listener { use crate::utilities::mocks::{Mock, ServerMock}; use super::*; - use glide_core::redis_request::command::{Args, ArgsArray}; - use glide_core::redis_request::{Command, Transaction}; + use command_request::{CommandRequest, RequestType}; + use glide_core::command_request::command::{Args, ArgsArray}; + use glide_core::command_request::{Command, Transaction}; use glide_core::response::{response, ConstantResponse, Response}; use glide_core::scripts_container::add_script; use protobuf::{EnumOrUnknown, Message}; use redis::{Cmd, ConnectionAddr, FromRedisValue, Value}; - use redis_request::{RedisRequest, RequestType}; use rstest::rstest; use std::mem::size_of; use tokio::{net::UnixListener, runtime::Builder}; @@ -89,7 +89,7 @@ mod socket_listener { } struct CommandComponents { - args: Vec, + args: Vec, request_type: EnumOrUnknown, args_pointer: bool, } @@ -241,11 +241,11 @@ mod socket_listener { command.request_type = components.request_type; if components.args_pointer { command.args = Some(Args::ArgsVecPointer(Box::leak(Box::new(components.args)) - as *mut Vec + as *mut Vec as u64)); } else { let mut args_array = ArgsArray::new(); - args_array.args = components.args.into_iter().map(|str| str.into()).collect(); + args_array.args.clone_from(&components.args); command.args = Some(Args::ArgsArray(args_array)); } command @@ -253,14 +253,14 @@ mod socket_listener { fn get_command_request( callback_index: u32, - args: Vec, + args: Vec, request_type: EnumOrUnknown, args_pointer: bool, - ) -> RedisRequest { - let mut request = RedisRequest::new(); + ) -> CommandRequest { + let mut request = CommandRequest::new(); request.callback_idx = callback_index; - request.command = Some(redis_request::redis_request::Command::SingleCommand( + request.command = Some(command_request::command_request::Command::SingleCommand( get_command(CommandComponents { args, request_type, @@ -270,7 +270,7 @@ mod socket_listener { request } - fn write_request(buffer: &mut Vec, socket: &mut UnixStream, request: RedisRequest) { + fn write_request(buffer: &mut Vec, socket: &mut UnixStream, request: CommandRequest) { write_message(buffer, request); socket.write_all(buffer).unwrap(); } @@ -279,7 +279,7 @@ mod socket_listener { buffer: &mut Vec, socket: &mut UnixStream, callback_index: u32, - args: Vec, + args: Vec, request_type: EnumOrUnknown, args_pointer: bool, ) { @@ -294,7 +294,7 @@ mod socket_listener { callback_index: u32, commands_components: Vec, ) { - let mut request = RedisRequest::new(); + let mut request = CommandRequest::new(); request.callback_idx = callback_index; let mut transaction = Transaction::new(); transaction.commands.reserve(commands_components.len()); @@ -303,7 +303,7 @@ mod socket_listener { transaction.commands.push(get_command(components)); } - request.command = Some(redis_request::redis_request::Command::Transaction( + request.command = Some(command_request::command_request::Command::Transaction( transaction, )); @@ -321,8 +321,8 @@ mod socket_listener { buffer, socket, callback_index, - vec![key.to_string()], - RequestType::GetString.into(), + vec![key.to_string().into()], + RequestType::Get.into(), args_pointer, ) } @@ -339,8 +339,8 @@ mod socket_listener { buffer, socket, callback_index, - vec![key.to_string(), value], - RequestType::SetString.into(), + vec![key.to_string().into(), value.into()], + RequestType::Set.into(), args_pointer, ) } @@ -575,6 +575,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_socket_set_and_get( #[values((false, Tls::NoTls), (true, Tls::NoTls), (false, Tls::UseTls))] @@ -621,6 +622,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_socket_pass_custom_command( #[values(false, true)] args_pointer: bool, @@ -640,7 +642,11 @@ mod socket_listener { &mut buffer, &mut test_basics.socket, CALLBACK1_INDEX, - vec!["SET".to_string(), key.to_string(), value.clone()], + vec![ + "SET".to_string().into(), + key.to_string().into(), + value.clone().into(), + ], RequestType::CustomCommand.into(), args_pointer, ); @@ -652,7 +658,7 @@ mod socket_listener { &mut buffer, &mut test_basics.socket, CALLBACK2_INDEX, - vec!["GET".to_string(), key], + vec!["GET".to_string().into(), key.into()], RequestType::CustomCommand.into(), args_pointer, ); @@ -675,12 +681,12 @@ mod socket_listener { let mut buffer = Vec::with_capacity(approx_message_length); let mut request = get_command_request( CALLBACK1_INDEX, - vec!["ECHO".to_string(), "foo".to_string()], + vec!["ECHO".to_string().into(), "foo".to_string().into()], RequestType::CustomCommand.into(), false, ); - let mut routes = redis_request::Routes::default(); - routes.set_simple_routes(redis_request::SimpleRoutes::AllPrimaries); + let mut routes = command_request::Routes::default(); + routes.set_simple_routes(command_request::SimpleRoutes::AllPrimaries); request.route = Some(routes).into(); write_request(&mut buffer, &mut test_basics.socket, request); @@ -723,12 +729,12 @@ mod socket_listener { let mut buffer = Vec::with_capacity(approx_message_length); let mut request = get_command_request( CALLBACK_INDEX, - vec!["CLUSTER".to_string(), "NODES".to_string()], + vec!["CLUSTER".to_string().into(), "NODES".to_string().into()], RequestType::CustomCommand.into(), false, ); - let mut routes = redis_request::Routes::default(); - routes.set_simple_routes(redis_request::SimpleRoutes::Random); + let mut routes = command_request::Routes::default(); + routes.set_simple_routes(command_request::SimpleRoutes::Random); request.route = Some(routes).into(); write_request(&mut buffer, &mut test_basics.socket, request.clone()); @@ -749,8 +755,8 @@ mod socket_listener { .unwrap(); buffer.clear(); - let mut routes = redis_request::Routes::default(); - let by_address_route = glide_core::redis_request::ByAddressRoute { + let mut routes = command_request::Routes::default(); + let by_address_route = glide_core::command_request::ByAddressRoute { host: host.into(), port, ..Default::default() @@ -809,7 +815,7 @@ mod socket_listener { &mut buffer, &mut test_basics.socket, CALLBACK_INDEX, - vec![key], + vec![key.into()], EnumOrUnknown::from_i32(request_type), false, ); @@ -828,6 +834,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_socket_send_and_receive_long_values( #[values((false, Tls::NoTls), (true, Tls::NoTls), (false, Tls::UseTls))] @@ -891,6 +898,7 @@ mod socket_listener { // This test starts multiple threads writing large inputs to a socket, and another thread that reads from the output socket and // verifies that the outputs match the inputs. #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_socket_send_and_receive_multiple_long_inputs( #[values((false, Tls::NoTls), (true, Tls::NoTls), (false, Tls::UseTls))] @@ -946,7 +954,6 @@ mod socket_listener { assert_eq!(results[callback_index], State::ReceivedNull); let values = values_for_read.lock().unwrap(); - assert_value( pointer, Some(Value::BulkString(values[callback_index].clone())), @@ -1026,6 +1033,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_does_not_close_when_server_closes() { let mut test_basics = @@ -1054,6 +1062,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_reconnect_after_temporary_disconnect() { let test_basics = setup_server_test_basics(Tls::NoTls, TestServer::Unique); @@ -1098,6 +1107,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_handle_request_after_reporting_disconnet() { let test_basics = setup_server_test_basics(Tls::NoTls, TestServer::Unique); @@ -1141,6 +1151,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_send_transaction_and_get_array_of_results( #[values(RedisType::Cluster, RedisType::Standalone)] use_cluster: RedisType, @@ -1152,24 +1163,24 @@ mod socket_listener { let key = generate_random_string(KEY_LENGTH); let commands = vec![ CommandComponents { - args: vec![key.clone(), "bar".to_string()], + args: vec![key.clone().into(), "bar".to_string().into()], args_pointer: true, - request_type: RequestType::SetString.into(), + request_type: RequestType::Set.into(), }, CommandComponents { - args: vec!["GET".to_string(), key.clone()], + args: vec!["GET".to_string().into(), key.clone().into()], args_pointer: false, request_type: RequestType::CustomCommand.into(), }, CommandComponents { - args: vec!["FLUSHALL".to_string()], + args: vec!["FLUSHALL".to_string().into()], args_pointer: false, request_type: RequestType::CustomCommand.into(), }, CommandComponents { - args: vec![key], + args: vec![key.into()], args_pointer: false, - request_type: RequestType::GetString.into(), + request_type: RequestType::Get.into(), }, ]; let mut buffer = Vec::with_capacity(200); @@ -1189,6 +1200,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_send_script( #[values(RedisType::Cluster, RedisType::Standalone)] use_cluster: RedisType, @@ -1199,15 +1211,15 @@ mod socket_listener { let key = generate_random_string(KEY_LENGTH); let value = generate_random_string(VALUE_LENGTH); let script = r#"redis.call("SET", KEYS[1], ARGV[1]); return redis.call("GET", KEYS[1])"#; - let hash = add_script(script); + let hash = add_script(script.as_bytes()); let approx_message_length = hash.len() + value.len() + key.len() + APPROX_RESP_HEADER_LEN; let mut buffer = Vec::with_capacity(approx_message_length); - let mut request = RedisRequest::new(); + let mut request = CommandRequest::new(); request.callback_idx = CALLBACK_INDEX; - request.command = Some(redis_request::redis_request::Command::ScriptInvocation( - redis_request::ScriptInvocation { + request.command = Some(command_request::command_request::Command::ScriptInvocation( + command_request::ScriptInvocation { hash: hash.into(), keys: vec![key.into()], args: vec![value.clone().into()], @@ -1226,6 +1238,7 @@ mod socket_listener { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] fn test_send_empty_custom_command_is_an_error( #[values(RedisType::Cluster, RedisType::Standalone)] use_cluster: RedisType, diff --git a/glide-core/tests/test_standalone_client.rs b/glide-core/tests/test_standalone_client.rs index 1d00ed5bbe..75e3262f80 100644 --- a/glide-core/tests/test_standalone_client.rs +++ b/glide-core/tests/test_standalone_client.rs @@ -1,19 +1,25 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ mod utilities; #[cfg(test)] mod standalone_client_tests { + use std::collections::HashMap; + use crate::utilities::mocks::{Mock, ServerMock}; use super::*; - use glide_core::{client::StandaloneClient, connection_request::ReadFrom}; + use glide_core::{ + client::{ConnectionError, StandaloneClient}, + connection_request::ReadFrom, + }; use redis::{FromRedisValue, Value}; use rstest::rstest; use utilities::*; #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_report_disconnect_and_reconnect_after_temporary_disconnect( #[values(false, true)] use_tls: bool, @@ -51,6 +57,7 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(LONG_STANDALONE_TEST_TIMEOUT)] #[cfg(standalone_heartbeat)] fn test_detect_disconnect_and_reconnect_using_heartbeat(#[values(false, true)] use_tls: bool) { @@ -85,10 +92,11 @@ mod standalone_client_tests { }); } - fn create_primary_mock_with_replicas(replica_count: usize) -> Vec { - let mut listeners: Vec = (0..replica_count + 1) - .map(|_| get_listener_on_available_port()) - .collect(); + fn get_mock_addresses(mocks: &[ServerMock]) -> Vec { + mocks.iter().flat_map(|mock| mock.get_addresses()).collect() + } + + fn create_primary_responses() -> HashMap { let mut primary_responses = std::collections::HashMap::new(); primary_responses.insert( "*1\r\n$4\r\nPING\r\n".to_string(), @@ -98,8 +106,10 @@ mod standalone_client_tests { "*2\r\n$4\r\nINFO\r\n$11\r\nREPLICATION\r\n".to_string(), Value::BulkString(b"role:master\r\nconnected_slaves:3\r\n".to_vec()), ); - let primary = ServerMock::new_with_listener(primary_responses, listeners.pop().unwrap()); - let mut mocks = vec![primary]; + primary_responses + } + + fn create_replica_response() -> HashMap { let mut replica_responses = std::collections::HashMap::new(); replica_responses.insert( "*1\r\n$4\r\nPING\r\n".to_string(), @@ -109,10 +119,32 @@ mod standalone_client_tests { "*2\r\n$4\r\nINFO\r\n$11\r\nREPLICATION\r\n".to_string(), Value::BulkString(b"role:slave\r\n".to_vec()), ); + replica_responses + } + fn create_primary_conflict_mock_two_primaries_one_replica() -> Vec { + let mut listeners: Vec = + (0..3).map(|_| get_listener_on_available_port()).collect(); + let primary_1 = + ServerMock::new_with_listener(create_primary_responses(), listeners.pop().unwrap()); + let primary_2 = + ServerMock::new_with_listener(create_primary_responses(), listeners.pop().unwrap()); + let replica = + ServerMock::new_with_listener(create_replica_response(), listeners.pop().unwrap()); + vec![primary_1, primary_2, replica] + } + + fn create_primary_mock_with_replicas(replica_count: usize) -> Vec { + let mut listeners: Vec = (0..replica_count + 1) + .map(|_| get_listener_on_available_port()) + .collect(); + let primary = + ServerMock::new_with_listener(create_primary_responses(), listeners.pop().unwrap()); + let mut mocks = vec![primary]; + mocks.extend( listeners .into_iter() - .map(|listener| ServerMock::new_with_listener(replica_responses.clone(), listener)), + .map(|listener| ServerMock::new_with_listener(create_replica_response(), listener)), ); mocks } @@ -154,8 +186,7 @@ mod standalone_client_tests { } } - let mut addresses: Vec = - mocks.iter().flat_map(|mock| mock.get_addresses()).collect(); + let mut addresses = get_mock_addresses(&mocks); for i in 4 - config.number_of_missing_replicas..4 { addresses.push(redis::ConnectionAddr::Tcp( @@ -168,7 +199,7 @@ mod standalone_client_tests { connection_request.read_from = config.read_from.into(); block_on_all(async { - let mut client = StandaloneClient::create_client(connection_request.into()) + let mut client = StandaloneClient::create_client(connection_request.into(), None) .await .unwrap(); for mock in mocks.drain(1..config.number_of_replicas_dropped_after_connection + 1) { @@ -193,12 +224,14 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_read_from_replica_always_read_from_primary() { test_read_from_replica(ReadFromReplicaTestConfig::default()); } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_read_from_replica_round_robin() { test_read_from_replica(ReadFromReplicaTestConfig { @@ -210,6 +243,7 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_read_from_replica_round_robin_skip_disconnected_replicas() { test_read_from_replica(ReadFromReplicaTestConfig { @@ -222,6 +256,7 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_read_from_replica_round_robin_read_from_primary_if_no_replica_is_connected() { test_read_from_replica(ReadFromReplicaTestConfig { @@ -234,6 +269,7 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_read_from_replica_round_robin_do_not_read_from_disconnected_replica() { test_read_from_replica(ReadFromReplicaTestConfig { @@ -247,6 +283,7 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_read_from_replica_round_robin_with_single_replica() { test_read_from_replica(ReadFromReplicaTestConfig { @@ -259,6 +296,34 @@ mod standalone_client_tests { }); } + #[rstest] + #[serial_test::serial] + #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] + fn test_primary_conflict_raises_error() { + let mocks = create_primary_conflict_mock_two_primaries_one_replica(); + let addresses = get_mock_addresses(&mocks); + let connection_request = + create_connection_request(addresses.as_slice(), &Default::default()); + block_on_all(async { + let client_res = StandaloneClient::create_client(connection_request.into(), None) + .await + .map_err(ConnectionError::Standalone); + assert!(client_res.is_err()); + let error = client_res.unwrap_err(); + assert!(matches!(error, ConnectionError::Standalone(_),)); + let primary_1_addr = addresses.first().unwrap().to_string(); + let primary_2_addr = addresses.get(1).unwrap().to_string(); + let replica_addr = addresses.get(2).unwrap().to_string(); + let err_msg = error.to_string().to_ascii_lowercase(); + assert!( + err_msg.contains("conflict") + && err_msg.contains(&primary_1_addr) + && err_msg.contains(&primary_2_addr) + && !err_msg.contains(&replica_addr) + ); + }); + } + #[rstest] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_send_acl_request_to_all_nodes() { @@ -279,7 +344,7 @@ mod standalone_client_tests { create_connection_request(addresses.as_slice(), &Default::default()); block_on_all(async { - let mut client = StandaloneClient::create_client(connection_request.into()) + let mut client = StandaloneClient::create_client(connection_request.into(), None) .await .unwrap(); @@ -293,6 +358,7 @@ mod standalone_client_tests { } #[rstest] + #[serial_test::serial] #[timeout(SHORT_STANDALONE_TEST_TIMEOUT)] fn test_set_database_id_after_reconnection() { let mut client_info_cmd = redis::Cmd::new(); diff --git a/glide-core/tests/utilities/cluster.rs b/glide-core/tests/utilities/cluster.rs index 6b524a9c51..9e7c356f4e 100644 --- a/glide-core/tests/utilities/cluster.rs +++ b/glide-core/tests/utilities/cluster.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use super::{create_connection_request, ClusterMode, TestConfiguration}; use futures::future::{join_all, BoxFuture}; @@ -122,7 +122,7 @@ impl RedisCluster { let (cluster_folder, addresses) = Self::parse_start_script_output(&stdout, &stderr); let mut password: Option = None; if let Some(info) = conn_info { - password = info.password.clone(); + password.clone_from(&info.password); }; RedisCluster { cluster_folder, @@ -229,18 +229,10 @@ async fn setup_acl_for_cluster( join_all(ops).await; } -pub async fn setup_test_basics_internal(mut configuration: TestConfiguration) -> ClusterTestBasics { - let cluster = if !configuration.shared_server { - Some(RedisCluster::new( - configuration.use_tls, - &configuration.connection_info, - None, - None, - )) - } else { - None - }; - +pub async fn create_cluster_client( + cluster: &Option, + mut configuration: TestConfiguration, +) -> Client { let addresses = if !configuration.shared_server { cluster.as_ref().unwrap().get_server_addresses() } else { @@ -257,7 +249,21 @@ pub async fn setup_test_basics_internal(mut configuration: TestConfiguration) -> configuration.request_timeout = configuration.request_timeout.or(Some(10000)); let connection_request = create_connection_request(&addresses, &configuration); - let client = Client::new(connection_request.into()).await.unwrap(); + Client::new(connection_request.into(), None).await.unwrap() +} + +pub async fn setup_test_basics_internal(configuration: TestConfiguration) -> ClusterTestBasics { + let cluster = if !configuration.shared_server { + Some(RedisCluster::new( + configuration.use_tls, + &configuration.connection_info, + None, + None, + )) + } else { + None + }; + let client = create_cluster_client(&cluster, configuration).await; ClusterTestBasics { cluster, client } } diff --git a/glide-core/tests/utilities/mocks.rs b/glide-core/tests/utilities/mocks.rs index f465000988..160e8a3189 100644 --- a/glide-core/tests/utilities/mocks.rs +++ b/glide-core/tests/utilities/mocks.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use futures_intrusive::sync::ManualResetEvent; use redis::{Cmd, ConnectionAddr, Value}; diff --git a/glide-core/tests/utilities/mod.rs b/glide-core/tests/utilities/mod.rs index f675b62c6b..05c6f1f05a 100644 --- a/glide-core/tests/utilities/mod.rs +++ b/glide-core/tests/utilities/mod.rs @@ -1,5 +1,5 @@ /* - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ #![allow(dead_code)] @@ -12,7 +12,7 @@ use once_cell::sync::Lazy; use rand::{distributions::Alphanumeric, Rng}; use redis::{ cluster_routing::{MultipleNodeRoutingInfo, RoutingInfo}, - ConnectionAddr, RedisConnectionInfo, RedisResult, Value, + ConnectionAddr, PushInfo, RedisConnectionInfo, RedisResult, Value, }; use socket2::{Domain, Socket, Type}; use std::{ @@ -20,6 +20,7 @@ use std::{ sync::Mutex, time::Duration, }; use tempfile::TempDir; +use tokio::sync::mpsc; pub mod cluster; pub mod mocks; @@ -456,7 +457,7 @@ pub async fn wait_for_server_to_become_ready(server_address: &ConnectionAddr) { }) .unwrap(); loop { - match client.get_multiplexed_async_connection().await { + match client.get_multiplexed_async_connection(None).await { Err(err) => { if err.is_connection_refusal() { tokio::time::sleep(millisecond).await; @@ -546,6 +547,7 @@ pub async fn send_set_and_get(mut client: Client, key: String) { pub struct TestBasics { pub server: Option, pub client: StandaloneClient, + pub push_receiver: mpsc::UnboundedReceiver, } fn convert_to_protobuf_protocol( @@ -592,7 +594,8 @@ pub async fn setup_acl(addr: &ConnectionAddr, connection_info: &RedisConnectionI }) .unwrap(); let mut connection = - repeat_try_create(|| async { client.get_multiplexed_async_connection().await.ok() }).await; + repeat_try_create(|| async { client.get_multiplexed_async_connection(None).await.ok() }) + .await; let password = connection_info.password.clone().unwrap(); let username = connection_info @@ -609,7 +612,7 @@ pub async fn setup_acl(addr: &ConnectionAddr, connection_info: &RedisConnectionI connection.send_packed_command(&cmd).await.unwrap(); } -#[derive(Eq, PartialEq, Default)] +#[derive(Eq, PartialEq, Default, Clone)] pub enum ClusterMode { #[default] Disabled, @@ -652,7 +655,7 @@ pub fn create_connection_request( connection_request } -#[derive(Default)] +#[derive(Default, Clone)] pub struct TestConfiguration { pub use_tls: bool, pub connection_retry_strategy: Option, @@ -689,11 +692,16 @@ pub(crate) async fn setup_test_basics_internal(configuration: &TestConfiguration let mut connection_request = create_connection_request(&[connection_addr], configuration); connection_request.cluster_mode_enabled = false; connection_request.protocol = configuration.protocol.into(); - let client = StandaloneClient::create_client(connection_request.into()) + let (push_sender, push_receiver) = tokio::sync::mpsc::unbounded_channel(); + let client = StandaloneClient::create_client(connection_request.into(), Some(push_sender)) .await .unwrap(); - TestBasics { server, client } + TestBasics { + server, + client, + push_receiver, + } } pub async fn setup_test_basics(use_tls: bool) -> TestBasics { diff --git a/go/Cargo.toml b/go/Cargo.toml index cd48809cfc..6d6c4ecb15 100644 --- a/go/Cargo.toml +++ b/go/Cargo.toml @@ -3,7 +3,7 @@ name = "glide-rs" version = "0.1.0" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] [lib] crate-type = ["cdylib"] diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index 6f365c756d..ab89b259b3 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -85,10 +85,15 @@ brew install go make git gcc pkgconfig protobuf@3 openssl export PATH="$PATH:$HOME/go/bin" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" -# Check that the protobuf compiler is installed. A minimum version of 3.20.0 is required. -protoc --version # Check that the Rust compiler is installed rustc --version +# Verify the Protobuf compiler installation +protoc --version + +# If protoc is not found or does not work correctly, update the PATH +echo 'export PATH="/opt/homebrew/opt/protobuf@3/bin:$PATH"' >> /Users/$USER/.bash_profile +source /Users/$USER/.bash_profile +protoc --version ``` #### Building and installation steps @@ -98,7 +103,7 @@ Before starting this step, make sure you've installed all software requirements. 1. Clone the repository: ```bash VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git + git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git cd glide-for-redis ``` 2. Initialize git submodule: diff --git a/go/Makefile b/go/Makefile index 4d4f132c82..a628643d1c 100644 --- a/go/Makefile +++ b/go/Makefile @@ -1,5 +1,5 @@ install-build-tools: - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.32.0 + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.33.0 install-dev-tools-go1.18.10: go install github.com/vakenbolt/go-test-report@v0.9.3 @@ -33,9 +33,9 @@ build-glide-client: generate-protobuf: mkdir -p protobuf protoc --proto_path=../glide-core/src/protobuf \ - --go_opt=Mconnection_request.proto=github.com/aws/glide-for-redis/go/protobuf \ - --go_opt=Mredis_request.proto=github.com/aws/glide-for-redis/go/protobuf \ - --go_opt=Mresponse.proto=github.com/aws/glide-for-redis/go/protobuf \ + --go_opt=Mconnection_request.proto=github.com/valkey-io/valkey-glide/go/protobuf \ + --go_opt=Mcommand_request.proto=github.com/valkey-io/valkey-glide/go/protobuf \ + --go_opt=Mresponse.proto=github.com/valkey-io/valkey-glide/go/protobuf \ --go_out=./protobuf \ --go_opt=paths=source_relative \ ../glide-core/src/protobuf/*.proto diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000000..cbab727b42 --- /dev/null +++ b/go/README.md @@ -0,0 +1,23 @@ +# GO wrapper + +The GO wrapper is currently not in a usable state and is under development. + +# Valkey GLIDE + +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. + +## Supported Engine Versions + +Refer to the [Supported Engine Versions table](https://github.com/valkey-io/valkey-glide/blob/main/README.md#supported-engine-versions) for details. + +## Current Status + +# Getting Started - GO Wrapper + +## GO supported version + +## Basic Example + +### Building & Testing + +Development instructions for local building & testing the package are in the [DEVELOPER.md](DEVELOPER.md) file. diff --git a/go/api/config.go b/go/api/config.go index 9d2417b429..cc20d5e7c8 100644 --- a/go/api/config.go +++ b/go/api/config.go @@ -1,8 +1,8 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 package api -import "github.com/aws/glide-for-redis/go/glide/protobuf" +import "github.com/valkey-io/valkey-glide/go/glide/protobuf" const ( defaultHost = "localhost" diff --git a/go/api/config_test.go b/go/api/config_test.go index 53a18e5308..44e6530d9d 100644 --- a/go/api/config_test.go +++ b/go/api/config_test.go @@ -1,4 +1,4 @@ -// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 package api @@ -6,8 +6,8 @@ import ( "fmt" "testing" - "github.com/aws/glide-for-redis/go/glide/protobuf" "github.com/stretchr/testify/assert" + "github.com/valkey-io/valkey-glide/go/glide/protobuf" ) func TestDefaultStandaloneConfig(t *testing.T) { diff --git a/go/cbindgen.toml b/go/cbindgen.toml index 8bd3eb749f..9378736cd1 100644 --- a/go/cbindgen.toml +++ b/go/cbindgen.toml @@ -1,7 +1,7 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 language = "C" -header = "/* Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */" +header = "/* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */" [parse] parse_deps = true diff --git a/go/go.mod b/go/go.mod index 29fff40d72..21aaf9f4a7 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,10 +1,10 @@ -module github.com/aws/glide-for-redis/go/glide +module github.com/valkey-io/valkey-glide/go/glide go 1.18 require ( github.com/stretchr/testify v1.8.4 - google.golang.org/protobuf v1.32.0 + google.golang.org/protobuf v1.33.0 ) require ( diff --git a/go/go.sum b/go/go.sum index 22f5ed3148..cdbe2b7522 100644 --- a/go/go.sum +++ b/go/go.sum @@ -15,8 +15,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go/src/lib.rs b/go/src/lib.rs index 72ffeca427..bd76ebe347 100644 --- a/go/src/lib.rs +++ b/go/src/lib.rs @@ -1,5 +1,5 @@ /* - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ // TODO: Investigate using uniffi bindings for Go instead of cbindgen @@ -81,7 +81,7 @@ fn create_client_internal( errors::error_message(&redis_error) })?; let client = runtime - .block_on(GlideClient::new(ConnectionRequest::from(request))) + .block_on(GlideClient::new(ConnectionRequest::from(request), None)) .map_err(|err| err.to_string())?; Ok(ClientAdapter { client, diff --git a/java/.gitignore b/java/.gitignore index a7b8d6a177..0c5c26d954 100644 --- a/java/.gitignore +++ b/java/.gitignore @@ -6,3 +6,9 @@ build # Ignore protobuf generated files protobuf + +# Must be ignored as part of Maven Central publishing process +gradle.properties + +# Ignore VSCode JUnit extension directories +bin/ diff --git a/java/.ort.yml b/java/.ort.yml new file mode 100644 index 0000000000..fa5555e99e --- /dev/null +++ b/java/.ort.yml @@ -0,0 +1,20 @@ +excludes: + paths: + - pattern: "java/benchmarks/**" + reason: "TEST_OF" + comment: >- + Licenses contained in this directory are used for benchmarks and do not apply to the OSS Review Toolkit. + - pattern: "java/integTest/**" + reason: "TEST_OF" + comment: >- + Licenses contained in this directory are used for testing and do not apply to the OSS Review Toolkit. + scopes: + - pattern: "test.*" + reason: "TEST_DEPENDENCY_OF" + comment: Packages for testing only. Not part of released artifacts." + - pattern: "(spotbugs.*|spotbugsSlf4j.*)" + reason: "TEST_DEPENDENCY_OF" + comment: Packages for static analysis only. Not part of released artifacts." + - pattern: "jacoco.*" + reason: "TEST_DEPENDENCY_OF" + comment: Packages for code coverage verification only. Not part of released artifacts." diff --git a/java/Cargo.toml b/java/Cargo.toml index 15d26a30e4..452b80856c 100644 --- a/java/Cargo.toml +++ b/java/Cargo.toml @@ -3,7 +3,7 @@ name = "glide-rs" version = "0.1.0" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -17,6 +17,7 @@ logger_core = {path = "../logger_core"} tracing-subscriber = "0.3.16" jni = "0.21.1" log = "0.4.20" +bytes = { version = "1.6.0" } [profile.release] lto = true diff --git a/java/DEVELOPER.md b/java/DEVELOPER.md new file mode 100644 index 0000000000..413a90f953 --- /dev/null +++ b/java/DEVELOPER.md @@ -0,0 +1,333 @@ +# Developer Guide + +This document describes how to set up your development environment to build and test the Valkey GLIDE Java wrapper. + +### Development Overview + +The Valkey GLIDE Java wrapper consists of both Java and Rust code. Rust bindings for the Java Native Interface are implemented using [jni-rs](https://github.com/jni-rs/jni-rs), and the Java JAR is built using [Gradle](https://github.com/gradle/gradle). The Java and Rust components communicate using the [protobuf](https://github.com/protocolbuffers/protobuf) protocol. + +### Build from source + +**Note:** See the [Troubleshooting](#troubleshooting) section below for possible solutions to problems. + +#### Prerequisites + +**Software Dependencies** + +- git +- GCC +- pkg-config +- protoc (protobuf compiler) >= 26.1 +- openssl +- openssl-dev +- rustup +- Java 11 + +**Dependencies installation for Ubuntu** + +```bash +sudo apt update -y +sudo apt install -y openjdk-11-jdk git gcc pkg-config openssl libssl-dev unzip +# Install rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +# Check that the Rust compiler is installed +rustc --version +``` + +Continue with **Install protobuf compiler** below. + +**Dependencies installation for CentOS** + +```bash +sudo yum update -y +sudo yum install -y java-11-openjdk-devel git gcc pkgconfig openssl openssl-devel unzip +# Install rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Continue with **Install protobuf compiler** below. + +**Dependencies installation for MacOS** + +```bash +brew update +brew install openjdk@11 git gcc pkgconfig protobuf openssl protobuf +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +``` + +Continue with **Install protobuf compiler** below. + +**Install protobuf compiler** + +To install protobuf for MacOS, run: +```bash +brew install protobuf +# Check that the protobuf compiler version 26.1 or higher is installed +protoc --version +``` + +For the remaining systems, do the following: +```bash +PB_REL="https://github.com/protocolbuffers/protobuf/releases" +curl -LO $PB_REL/download/v26.1/protoc-26.1-linux-x86_64.zip +unzip protoc-26.1-linux-x86_64.zip -d $HOME/.local +export PATH="$PATH:$HOME/.local/bin" +# Check that the protobuf compiler version 26.1 or higher is installed +protoc --version +``` + +#### Building and installation steps + +Before starting this step, make sure you've installed all software dependencies. + +1. Clone the repository: + ```bash + VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch + git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git + cd valkey-glide/java + ``` +2. Initialize git submodule: + ```bash + git submodule update --init --recursive + ``` +3. Build the Java wrapper (Choose a build option from the following and run it from the `java` folder): + + 1. Enter the java directory: + + ```bash + cd java + ``` + + 2. Build in debug mode: + + ```bash + ./gradlew :client:buildAll + ``` + + 3. Build in release mode: + + ```bash + ./gradlew :client:buildAllRelease + ``` + +### Linters + +Development on the Java wrapper may involve changes in either the Java or Rust code. Each language has distinct linter tests that must be passed before committing changes. + +Firstly, install the Rust linter +```bash +# Run from the `java` folder +# Will only need to run once during the installation process +rustup component add clippy rustfmt +cargo clippy --all-features --all-targets -- -D warnings +cargo fmt --manifest-path ./Cargo.toml --all +``` + +#### Language-specific Linters and Static Code Analysis + +**Java:** +For Java, we use Spotless and SpotBugs. + +1. Spotless + + ```bash + # Run from the `java` folder + ./gradlew :spotlessCheck # run first to see if there are any linting changes to make + ./gradlew :spotlessApply # to apply these changes + ``` + +2. SpotBugs + + To run SpotBugs and generate reports: + + ```bash + # Run from the `java` folder + ./gradlew spotbugsMain + ``` + + This command will generate HTML and XML reports in the `build/reports/spotbugs/` directory. + + To view the SpotBugs findings: + - Open the HTML report located at `build/reports/spotbugs/main/spotbugs.html` in a web browser. + - If you are using IntellJ Idea - open `build/reports/spotbugs/main/spotbugs.xml` in SpotBugs plugin as it will provide better experience. + + Ensure any new findings are addressed and fixed before committing and pushing your changes. + + _Note: The `spotbugs` task is currently configured to not fail the build on findings._ + +### Troubleshooting + +Some troubleshooting issues: +- If the build fails after following the installation instructions, the `gradle` daemon may need to be + restarted (`./gradlew --stop`) so that it recognizes changes to environment variables (e.g. `$PATH`). If that doesn't work, + you may need to restart your machine. In particular, this may solve the following problems: + - Failed to find `cargo` after `rustup`. + - No Protobuf compiler (protoc) found. +- If build fails because of rust compiler fails, make sure submodules are updated using `git submodule update`. +- If protobuf 26.0 or earlier is detected, upgrade to the latest protobuf release. + +## Running Examples App + +An example app (`glide.examples.ExamplesApp`) is available under [examples project](../examples/java). To run the ExamplesApp against a local build of valkey-glide client, you can publish your JAR to local Maven repository as a dependency. + +To publish to local maven run (default version `255.255.255`): +```bash +# Run from the `examples/java` folder +./gradlew publishToMavenLocal +``` + +You can then add the valkey-glide dependency to [examples project](../examples/java/build.gradle): +```gradle +repositories { + mavenLocal() +} +dependencies { + // Update to use version defined in the previous step + implementation group: 'io.valkey', name: 'valkey-glide', version: '255.255.255' +} +``` + +Optionally: you can specify a snapshot release: + +```bash +export GLIDE_RELEASE_VERSION=1.0.1-SNAPSHOT +./gradlew publishToMavenLocal +``` + +You can then add the valkey-glide dependency to [examples/java/build.gradle](../examples/java/build.gradle) with the version and classifier: +```gradle +repositories { + mavenLocal() +} +dependencies { + // Update to use version defined in the previous step + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1-SNAPSHOT', classifier='osx-aarch_64' +} +``` + +### Test + +To run all tests, use the following command: + +```bash +./gradlew test +``` + +To run the unit tests, use the following command: + +```bash +./gradlew :client:test +``` + +To run FFI tests between Java and Rust, use the following command: + +```bash +./gradlew :client:testFfi +``` + +To run end-to-end tests, use the following command: + +```bash +./gradlew :integTest:test +``` + +To run a single test, use the following command: +```bash +./gradlew :integTest:test --tests '*.functionLoad_and_functionList' --rerun +``` + +To run one class, use the following command: +```bash +./gradlew :client:test --tests 'TransactionTests' --rerun +``` + +### Generate files +To (re)generate protobuf code, use the following command: + +```bash +./gradlew protobuf +``` + +### Submodules + +After pulling new changes, ensure that you update the submodules by running the following command: + +```bash +git submodule update +``` + +### Contributing new ValKey commands + +A Valkey command can either have a standalone or cluster implementation which is dependent on their specifications. +- A node is an instance of a Valkey server, and a valkey cluster is composed of multiple nodes working in tandem. +- A cluster command will require a note to indicate a node will follow a specific routing. +Refer to https://valkey.io/docs/topics/cluster-spec for more details on how hash slots work for cluster commands. + +When you start implementing a new command, check the [command_request.proto](https://github.com/valkey-io/valkey-glide/blob/main/glide-core/src/protobuf/command_request.proto) and [request_type.rs](https://github.com/valkey-io/valkey-glide/blob/main/glide-core/src/request_type.rs) files to see whether the command has already been implemented in another language such as Python or Node.js. + +Standalone and cluster clients both extend [BaseClient.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/main/java/glide/api/BaseClient.java) and implement methods from the interfaces listed in `java/client/src/main/java/glide/api/commands`. +The return types of these methods are in the form of a `CompletableFuture`, which fulfill the purpose of the asynchronous features of the program. + +### Tests + +When implementing a command, include both a unit test and an integration test. + +Implement unit tests in the following files: +- [GlideClientTest.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/test/java/glide/api/GlideClientTest.java) for standalone commands. +- [GlideClusterClientTest.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/test/java/glide/api/GlideClusterClientTest.java) for cluster commands. +These files are found in the java/client/src/test/java/glide/api path. + +Implement integration tests in the following files: +- [TransactionTests.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/test/java/glide/api/models/TransactionTests.java) (standalone and cluster). +- [TransactionTestsUtilities.java](https://github.com/valkey-io/valkey-glide/blob/main/java/integTest/src/test/java/glide/TransactionTestUtilities.java) (standalone and cluster). +- [SharedCommandTests.java](https://github.com/valkey-io/valkey-glide/blob/main/java/integTest/src/test/java/glide/SharedCommandTests.java) (standalone and cluster). +- [cluster/CommandTests.java](https://github.com/valkey-io/valkey-glide/blob/main/java/integTest/src/test/java/glide/cluster/CommandTests.java) (cluster). +- [standalone/CommandTests.java](https://github.com/valkey-io/valkey-glide/blob/main/java/integTest/src/test/java/glide/standalone/CommandTests.java) (standalone). +For commands that have options, create a separate file for the optional values. + +[BaseTransaction.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/main/java/glide/api/models/BaseTransaction.java) will add the command to the Transactions API. +Refer to [this](https://github.com/valkey-io/valkey-glide/tree/main/java/client/src/main/java/glide/api/commands) link to view the interface directory. +Refer to https://valkey.io/docs/topics/transactions/ for more details about how Transactions work in Valkey. + +### Javadocs + +[BaseTransaction.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/main/java/glide/api/models/BaseTransaction.java) and the methods within the command interfaces will both contain documentation on how the command operates. +In the command interface each command's javadoc should contain: +- Detail on when Valkey started supporting the command (if it wasn't initially implemented in 6.0.0 or before). +- A link to the Valkey documentation. +- Information about the function parameters. +- Any glide-core implementation details, such as how glide-core manages default routing for the command. Reference this [link](https://github.com/valkey-io/valkey-glide/blob/4df0dd939b515dbf9da0a00bfca6d3ad2f27440b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java#L119) for an example. +- The command's return type. In the [BaseTransaction.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/main/java/glide/api/models/BaseTransaction.java) file, include "Command Response" before specifying the return type. + +### Previous PR's + +Refer to [closed-PRs](https://github.com/valkey-io/valkey-glide/pulls?q=is%3Apr+is%3Aclosed+label%3Ajava) to see commands that have been previously merged. + +### FFI naming and signatures, and features + +Javac will create the name of the signature in Rust convention which can be called on native code. +- In the command line write: +```bash +javac -h . GlideValueResolver.java +``` +The results can be found in the `glide_ffi_resolvers_GlideValueResolver` file once the `javac -h. GlideValueResolver.java` command is ran. +In this project, only the function name and signature name is necessary. lib.rs method names explicitly point to the native functions defined there. + +### Module Information + +- The [module-info.java](https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/main/java/module-info.java) (glide.api) contains a list of all of the directories the user can access. +- Ensure to update the exports list if there are more directories the user will need to access. + +### Recommended extensions for VS Code + +- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - Rust language support for VSCode. +- [spotless-gradle](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) - Spotless Gradle plugin for VSCode. +- [gradle](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle) - Gradle extension for Java. + +### Recommended extensions for IntelliJ + +- [spotless-gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle) - Spotless Gradle plugin for IntelliJ. +- [lombok](https://plugins.jetbrains.com/plugin/6317-lombok) - Lombok plugin for IntelliJ. +- [SpotBugs](https://plugins.jetbrains.com/plugin/14014-spotbugs) - SpotBugs plugin for IntelliJ. diff --git a/java/README.md b/java/README.md index b7b9a3dcaa..aaa29dd0be 100644 --- a/java/README.md +++ b/java/README.md @@ -1,111 +1,253 @@ +# Valkey GLIDE + +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. + # Getting Started - Java Wrapper -## Notice: Java Wrapper - Work in Progress +## System Requirements -We're excited to share that the Java client is currently in development! However, it's important to note that this client -is a work in progress and is not yet complete or fully tested. Your contributions and feedback are highly encouraged as -we work towards refining and improving this implementation. Thank you for your interest and understanding as we continue -to develop this Java wrapper. +The release of Valkey GLIDE was tested on the following platforms: -The Java client contains the following parts: +Linux: +- Ubuntu 22.04.1 (x86_64) +- Amazon Linux 2023 (AL2023) (x86_64) -1. `client`: A Java-wrapper around the rust-core client. -2. `examples`: An examples app to test the client against a Redis localhost -3. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. -4. `integTest`: An integration test sub-project for API and E2E testing +macOS: +- macOS 12.7 (Apple silicon/aarch_64 and Intel/x86_64) -## Installation and Setup +## Layout of Java code +The Java client contains the following parts: -### Install from Gradle +1. `src`: Rust dynamic library FFI to integrate with [GLIDE core library](../glide-core/). +2. `client`: A Java-wrapper around the GLIDE core rust library and unit tests for it. +3. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of Valkey GLIDE and other Java clients. +4. `integTest`: An integration test sub-project for API and E2E testing. -At the moment, the Java client must be built from source. +An example app (called glide.examples.ExamplesApp) is also available under [examples app](../examples/java), to sanity check the project. -### Build from source +## Supported Engine Versions -Software Dependencies: +Refer to the [Supported Engine Versions table](https://github.com/valkey-io/valkey-glide/blob/main/README.md#supported-engine-versions) for details. -- JDK 11+ -- git -- protoc (protobuf compiler) -- Rust +## Installation and Setup #### Prerequisites -**Dependencies installation for Ubuntu** -```bash -sudo apt update -y -sudo apt install -y protobuf-compiler openjdk-11-jdk openssl gcc -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source "$HOME/.cargo/env" -``` +For developers, please refer to Java's [DEVELOPER.md](./DEVELOPER.md) for further instruction on how to set up your development environment. -**Dependencies for MacOS** +**Java Requirements** -Ensure that you have a minimum Java version of JDK 11 installed on your system: -```bash - $ echo $JAVA_HOME -/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home +Minimum requirements: JDK 11 or later. Ensure that you have a minimum Java version of JDK 11 installed on your system: -$ java -version - openjdk version "11.0.1" 2018-10-16 - OpenJDK Runtime Environment 18.9 (build 11.0.1+13) - OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode) +```bash +echo $JAVA_HOME +java -version ``` -#### Building and installation steps -The Java client is currently a work in progress and offers no guarantees. Users should build at their own risk. +### Adding the client to your project -Before starting this step, make sure you've installed all software requirements. -1. Clone the repository: -```bash -VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch -git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git -cd glide-for-redis +Refer to https://central.sonatype.com/artifact/io.valkey/valkey-glide. +Once set up, you can run the basic examples. + +Additionally, consider installing the Gradle plugin, [OS Detector](https://github.com/google/osdetector-gradle-plugin) to help you determine what classifier to use. + +## Classifiers +There are 4 types of classifiers for Valkey GLIDE which are ``` -2. Initialize git submodule: -```bash -git submodule update --init --recursive +osx-aarch_64 +osx-x86_64 +linux-aarch_64 +linux-x86_64 ``` -3. Generate protobuf files: -```bash -cd java/ -./gradlew :client:protobuf + +Gradle: +- Copy the snippet and paste it in the `build.gradle` dependencies section. +- **IMPORTANT** must include a `classifier` to specify your platform. +```groovy +// osx-aarch_64 +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'osx-aarch_64' +} + +// osx-x86_64 +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'osx-x86_64' +} + +// linux-aarch_64 +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'linux-aarch_64' +} + +// linux-x86_64 +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: 'linux-x86_64' +} + +// with osdetector +plugins { + id "com.google.osdetector" version "1.7.3" +} +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.0.1', classifier: osdetector.classifier +} ``` -4. Build the client library: -```bash -cd java/ -./gradlew :client:build + +Maven: +- **IMPORTANT** must include a `classifier`. Please use this dependency block and add it to the pom.xml file. +```xml + + + + io.valkey + valkey-glide + osx-aarch_64 + 1.0.1 + + + + + io.valkey + valkey-glide + osx-x86_64 + 1.0.1 + + + + + io.valkey + valkey-glide + linux-aarch_64 + 1.0.1 + + + + + io.valkey + valkey-glide + linux-x86_64 + 1.0.1 + ``` -5. Run tests: -```bash -cd java/ -$ ./gradlew :client:test + +## Setting up the Java module + +To use Valkey GLIDE in a Java project with modules, include a module-info.java in your project. + +For example, if your program is called `App`, you can follow this path +```java +app/src/main/java/module-info.java ``` -Other useful gradle developer commands: -* `./gradlew :client:test` to run client unit tests -* `./gradlew :integTest:test` to run client examples -* `./gradlew spotlessCheck` to check for codestyle issues -* `./gradlew spotlessApply` to apply codestyle recommendations -* `./gradlew :examples:run` to run client examples -* `./gradlew :benchmarks:run` to run performance benchmarks +and inside the module it will specifically require the line +`requires glide.api;` + +For example, if your project has a module called playground, it would look like this +```java +module playground { + requires glide.api; +} +``` ## Basic Examples -### Standalone Redis: +### Standalone Valkey: ```java -import glide.api.RedisClient; +// You can run this example code in Main.java. +import glide.api.GlideClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.GlideClientConfiguration; +import java.util.concurrent.ExecutionException; -RedisClient client = RedisClient.CreateClient().get(); +import static glide.api.models.GlideString.gs; -CompletableFuture setResponse = client.set("key", "foobar"); -assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; +public class Main { -CompletableFuture getResponse = client.get("key"); -assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; + public static void main(String[] args) { + runGlideExamples(); + } + + private static void runGlideExamples() { + String host = "localhost"; + Integer port = 6379; + boolean useSsl = false; + + GlideClientConfiguration config = + GlideClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port).build()) + .useTLS(useSsl) + .build(); + + try (GlideClient client = GlideClient.createClient(config).get()) { + + System.out.println("PING: " + client.ping(gs("PING")).get()); + System.out.println("PING(found you): " + client.ping( gs("found you")).get()); + + System.out.println("SET(apples, oranges): " + client.set(gs("apples"), gs("oranges")).get()); + System.out.println("GET(apples): " + client.get(gs("apples")).get()); + + } catch (ExecutionException | InterruptedException e) { + System.out.println("Glide example failed with an exception: "); + e.printStackTrace(); + } + } +} ``` +### Cluster Valkey: +```java +// You can run this example code in Main.java. +import glide.api.GlideClusterClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.GlideClusterClientConfiguration; +import glide.api.models.configuration.RequestRoutingConfiguration; + +import java.util.concurrent.ExecutionException; + +import static glide.api.models.GlideString.gs; + +public class Main { + + public static void main(String[] args) { + runGlideExamples(); + } + + private static void runGlideExamples() { + String host = "localhost"; + Integer port1 = 7001; + Integer port2 = 7002; + Integer port3 = 7003; + Integer port4 = 7004; + Integer port5 = 7005; + Integer port6 = 7006; + boolean useSsl = false; + + GlideClusterClientConfiguration config = + GlideClusterClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port1).port(port2).port(port3).port(port4).port(port5).port(port6).build()) + .useTLS(useSsl) + .build(); + + try (GlideClusterClient client = GlideClusterClient.createClient(config).get()) { + + System.out.println("PING: " + client.ping(gs("PING")).get()); + System.out.println("PING(found you): " + client.ping( gs("found you")).get()); + + System.out.println("SET(apples, oranges): " + client.set(gs("apples"), gs("oranges")).get()); + System.out.println("GET(apples): " + client.get(gs("apples")).get()); + + } catch (ExecutionException | InterruptedException e) { + System.out.println("Glide example failed with an exception: "); + e.printStackTrace(); + } + } +} +``` + +### Accessing tests +For more examples, you can refer to the test folder [unit tests](./client/src/test/java/glide/api) and [integration tests](./integTest/src/test/java/glide). + ### Benchmarks You can run benchmarks using `./gradlew run`. You can set arguments using the args flag like: @@ -117,9 +259,9 @@ You can run benchmarks using `./gradlew run`. You can set arguments using the ar The following arguments are accepted: * `resultsFile`: the results output file -* `concurrentTasks`: Number of concurrent tasks +* `concurrentTasks`: number of concurrent tasks * `clients`: one of: all|jedis|lettuce|glide -* `clientCount`: Client count -* `host`: redis server host url -* `port`: redis server port number -* `tls`: redis TLS configured +* `clientCount`: client count +* `host`: Valkey server host url +* `port`: Valkey server port number +* `tls`: Valkey TLS configured diff --git a/java/THIRD_PARTY_LICENSES_JAVA b/java/THIRD_PARTY_LICENSES_JAVA new file mode 100644 index 0000000000..a363964613 --- /dev/null +++ b/java/THIRD_PARTY_LICENSES_JAVA @@ -0,0 +1,39696 @@ +This software includes external packages and source code. +The applicable license information is listed below: + +---- + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +This software depends on external packages and source code. +The applicable license information is listed below: + +---- + +Package: addr2line:0.22.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: adler:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + -- + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: ahash:0.8.11 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: allocator-api2:0.2.18 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: android-tzdata:0.1.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: android_system_properties:0.1.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: arc-swap:1.7.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: arcstr:1.2.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + +Package: async-trait:0.1.81 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: backoff:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: backtrace:0.3.73 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: base64:0.22.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: bitflags:2.6.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: bumpalo:3.16.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: bytes:1.6.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: cesu8:1.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: cfg-if:1.0.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: chrono:0.4.38 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: combine:4.6.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: core-foundation:0.9.4 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: core-foundation-sys:0.8.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: crc16:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: crc32fast:1.4.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: crossbeam-channel:0.5.13 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: crossbeam-utils:0.8.20 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: deranged:0.3.11 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: derivative:2.2.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: directories:4.0.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: dirs-sys:0.3.7 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: dispose:0.5.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: dispose-derive:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: fast-math:0.1.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: file-rotate:0.7.6 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: flate2:1.0.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: form_urlencoded:1.2.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-channel:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-core:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-executor:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-intrusive:0.5.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-io:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-macro:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-sink:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-task:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: futures-util:0.3.30 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: getrandom:0.2.15 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: gimli:0.29.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: glide-core:0.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: hashbrown:0.14.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: heck:0.5.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: hermit-abi:0.3.9 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: iana-time-zone:0.1.60 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: iana-time-zone-haiku:0.1.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: idna:0.5.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: ieee754:0.2.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: instant:0.1.13 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: integer-encoding:4.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: itoa:1.0.11 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: jni:0.21.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: jni-sys:0.3.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: js-sys:0.3.69 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: lazy_static:1.5.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: libc:0.2.155 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: libredox:0.1.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: lock_api:0.4.12 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: log:0.4.22 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: logger_core:0.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: memchr:2.7.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +---- + +Package: miniz_oxide:0.7.4 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + +Package: mio:0.8.11 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: nanoid:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: nu-ansi-term:0.46.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: num-bigint:0.4.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: num-conv:0.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: num-integer:0.1.46 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: num-traits:0.2.19 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: num_cpus:1.16.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object:0.36.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: once_cell:1.19.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: openssl-probe:0.1.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: overload:0.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: parking_lot:0.12.3 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: parking_lot_core:0.9.10 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: percent-encoding:2.3.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: pin-project:1.1.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: pin-project-internal:1.1.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: pin-project-lite:0.2.14 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: pin-utils:0.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: powerfmt:0.2.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: ppv-lite86:0.2.17 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: proc-macro-error:1.0.4 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: proc-macro-error-attr:1.0.4 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: proc-macro2:1.0.86 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: protobuf:3.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: protobuf-support:3.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: quote:1.0.36 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rand:0.8.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rand_chacha:0.3.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rand_core:0.6.4 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: redis:0.25.2 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: redox_syscall:0.5.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: redox_users:0.4.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustc-demangle:0.1.24 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustls:0.22.4 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustls-native-certs:0.7.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustls-pemfile:2.1.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustls-pki-types:1.7.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: rustls-webpki:0.102.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +---- + +Package: rustversion:1.0.17 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: ryu:1.0.18 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +---- + +Package: schannel:0.1.23 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: scopeguard:1.2.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: security-framework:2.11.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: security-framework-sys:2.11.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: serde:1.0.204 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: serde_derive:1.0.204 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: sha1_smol:1.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: sharded-slab:0.1.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: slab:0.4.9 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: smallvec:1.13.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: socket2:0.5.7 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: spin:0.9.8 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: strum:0.26.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: strum_macros:0.26.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: subtle:2.6.1 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: syn:1.0.109 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: syn:2.0.71 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: thiserror:1.0.62 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: thiserror-impl:1.0.62 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: thread_local:1.1.8 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: time:0.3.36 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: time-core:0.1.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: time-macros:0.2.18 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tinyvec:1.8.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + +Package: tinyvec_macros:0.1.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +---- + +Package: tokio:1.38.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tokio-macros:2.3.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tokio-retry:0.3.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tokio-rustls:0.25.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tokio-util:0.7.11 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tracing:0.1.40 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tracing-appender:0.2.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tracing-attributes:0.1.27 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tracing-core:0.1.32 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tracing-log:0.2.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: tracing-subscriber:0.3.18 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: unicode-bidi:0.3.15 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: unicode-ident:1.0.12 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE + +Unicode Data Files include all data files under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu- +project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +Unicode Data Files do not include PDF online code charts under the +directory http://www.unicode.org/Public/. + +Software includes any source code published in the Unicode Standard or +under the directories http://www.unicode.org/Public/, +http://www.unicode.org/reports/, http://www.unicode.org/cldr/data/, +http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA +FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY +ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF +THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, +DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2016 Unicode, Inc. All rights reserved. Distributed +under the Terms of Use in http://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Unicode data files and any associated documentation (the +"Data Files") or Unicode software and any associated documentation (the +"Software") to deal in the Data Files or Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, and/or sell copies of the Data Files or Software, +and to permit persons to whom the Data Files or Software are furnished +to do so, provided that either + +(a) this copyright and permission notice appear with all copies of the +Data Files or Software, or + +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR +ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or +other dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +---- + +Package: unicode-normalization:0.1.23 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: untrusted:0.9.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +---- + +Package: url:2.5.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: valuable:0.1.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: wasi:0.11.0+wasi-snapshot-preview1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + + 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 [yyyy] [name of copyright owner] + + 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. + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: wasm-bindgen:0.2.92 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: wasm-bindgen-backend:0.2.92 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: wasm-bindgen-macro:0.2.92 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: wasm-bindgen-macro-support:0.2.92 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: wasm-bindgen-shared:0.2.92 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: winapi:0.3.9 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: winapi-i686-pc-windows-gnu:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: winapi-x86_64-pc-windows-gnu:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-core:0.52.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-sys:0.45.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-sys:0.48.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-sys:0.52.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-targets:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-targets:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows-targets:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_aarch64_gnullvm:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_aarch64_gnullvm:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_aarch64_gnullvm:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_aarch64_msvc:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_aarch64_msvc:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_aarch64_msvc:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_gnu:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_gnu:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_gnu:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_gnullvm:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_msvc:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_msvc:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_i686_msvc:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_gnu:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_gnu:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_gnu:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_gnullvm:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_gnullvm:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_gnullvm:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_msvc:0.42.2 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_msvc:0.48.5 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: windows_x86_64_msvc:0.52.6 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: zerocopy:0.7.35 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: zerocopy-derive:0.7.35 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: zeroize:1.8.1 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: com.google.protobuf:protobuf-java:4.27.1 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: io.netty:netty-buffer:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-codec:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-common:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-handler:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-resolver:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-transport:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-transport-classes-epoll:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-transport-classes-kqueue:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-transport-native-epoll:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-transport-native-kqueue:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: io.netty:netty-transport-native-unix-common:4.1.100.Final + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: org.apache.commons:commons-lang3:3.13.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: org.projectlombok:lombok:1.18.30 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: org.projectlombok:lombok:1.18.32 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java index 594c82c030..31ab7bbd13 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks; import static glide.benchmarks.utils.Benchmarking.testClientSetGet; diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/AsyncClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/AsyncClient.java index ce450bd118..8a6c8a9025 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/AsyncClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/AsyncClient.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.clients; import java.util.concurrent.ExecutionException; diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/Client.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/Client.java index 790229d9ec..1db6466659 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/Client.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/Client.java @@ -1,11 +1,11 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.clients; import glide.benchmarks.utils.ConnectionSettings; /** A Redis client interface */ public interface Client { - void connectToRedis(ConnectionSettings connectionSettings); + void connectToValkey(ConnectionSettings connectionSettings); default void closeConnection() {} diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/SyncClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/SyncClient.java index 4a47e6ed3d..f8034435ae 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/SyncClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/SyncClient.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.clients; /** A Redis client with sync capabilities */ diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java index ee2bdeb83a..e08c66840d 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/glide/GlideAsyncClient.java @@ -1,14 +1,14 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.clients.glide; import static java.util.concurrent.TimeUnit.SECONDS; import glide.api.BaseClient; -import glide.api.RedisClient; -import glide.api.RedisClusterClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; -import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.benchmarks.clients.AsyncClient; import glide.benchmarks.utils.ConnectionSettings; import java.util.concurrent.CompletableFuture; @@ -17,14 +17,14 @@ /** A Glide client with async capabilities */ public class GlideAsyncClient implements AsyncClient { - private BaseClient redisClient; + private BaseClient glideClient; @Override - public void connectToRedis(ConnectionSettings connectionSettings) { + public void connectToValkey(ConnectionSettings connectionSettings) { if (connectionSettings.clusterMode) { - RedisClusterClientConfiguration config = - RedisClusterClientConfiguration.builder() + GlideClusterClientConfiguration config = + GlideClusterClientConfiguration.builder() .address( NodeAddress.builder() .host(connectionSettings.host) @@ -33,14 +33,14 @@ public void connectToRedis(ConnectionSettings connectionSettings) { .useTLS(connectionSettings.useSsl) .build(); try { - redisClient = RedisClusterClient.CreateClient(config).get(10, SECONDS); + glideClient = GlideClusterClient.createClient(config).get(10, SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new RuntimeException(e); } } else { - RedisClientConfiguration config = - RedisClientConfiguration.builder() + GlideClientConfiguration config = + GlideClientConfiguration.builder() .address( NodeAddress.builder() .host(connectionSettings.host) @@ -50,7 +50,7 @@ public void connectToRedis(ConnectionSettings connectionSettings) { .build(); try { - redisClient = RedisClient.CreateClient(config).get(10, SECONDS); + glideClient = GlideClient.createClient(config).get(10, SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new RuntimeException(e); } @@ -59,18 +59,18 @@ public void connectToRedis(ConnectionSettings connectionSettings) { @Override public CompletableFuture asyncSet(String key, String value) { - return redisClient.set(key, value); + return glideClient.set(key, value); } @Override public CompletableFuture asyncGet(String key) { - return redisClient.get(key); + return glideClient.get(key); } @Override public void closeConnection() { try { - redisClient.close(); + glideClient.close(); } catch (ExecutionException e) { throw new RuntimeException(e); } diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java index 0553cbba37..3ca14d1252 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.clients.jedis; import glide.benchmarks.clients.SyncClient; @@ -32,7 +32,7 @@ public String getName() { } @Override - public void connectToRedis(ConnectionSettings connectionSettings) { + public void connectToValkey(ConnectionSettings connectionSettings) { isClusterMode = connectionSettings.clusterMode; if (isClusterMode) { jedisCluster = diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java index e628ed8f8c..c66507a94f 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/lettuce/LettuceAsyncClient.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.clients.lettuce; import glide.benchmarks.clients.AsyncClient; @@ -23,7 +23,7 @@ public class LettuceAsyncClient implements AsyncClient { private StatefulConnection connection; @Override - public void connectToRedis(ConnectionSettings connectionSettings) { + public void connectToValkey(ConnectionSettings connectionSettings) { RedisURI uri = RedisURI.builder() .withHost(connectionSettings.host) diff --git a/java/benchmarks/src/main/java/glide/benchmarks/utils/Benchmarking.java b/java/benchmarks/src/main/java/glide/benchmarks/utils/Benchmarking.java index 82bd607a70..514db973f6 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/utils/Benchmarking.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/utils/Benchmarking.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.utils; import glide.benchmarks.BenchmarkingApp; @@ -137,7 +137,7 @@ public static void testClientSetGet( List clients = new LinkedList<>(); for (int cc = 0; cc < clientCount; cc++) { Client newClient = clientCreator.get(); - newClient.connectToRedis( + newClient.connectToValkey( new ConnectionSettings( config.host, config.port, config.tls, config.clusterModeEnabled)); clients.add(newClient); diff --git a/java/benchmarks/src/main/java/glide/benchmarks/utils/ChosenAction.java b/java/benchmarks/src/main/java/glide/benchmarks/utils/ChosenAction.java index 90d62ba392..58c88ca08d 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/utils/ChosenAction.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/utils/ChosenAction.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.utils; public enum ChosenAction { diff --git a/java/benchmarks/src/main/java/glide/benchmarks/utils/ConnectionSettings.java b/java/benchmarks/src/main/java/glide/benchmarks/utils/ConnectionSettings.java index f15338bd01..e8eae01a1b 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/utils/ConnectionSettings.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/utils/ConnectionSettings.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.utils; /** Redis-client settings */ diff --git a/java/benchmarks/src/main/java/glide/benchmarks/utils/JsonWriter.java b/java/benchmarks/src/main/java/glide/benchmarks/utils/JsonWriter.java index c41ca18906..fb8004d69c 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/utils/JsonWriter.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/utils/JsonWriter.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.utils; import com.google.gson.Gson; diff --git a/java/benchmarks/src/main/java/glide/benchmarks/utils/LatencyResults.java b/java/benchmarks/src/main/java/glide/benchmarks/utils/LatencyResults.java index f7214f9865..297a1e42d0 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/utils/LatencyResults.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/utils/LatencyResults.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.benchmarks.utils; import java.util.Arrays; diff --git a/java/build.gradle b/java/build.gradle index 1e8824a15f..d36a4bf750 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -79,7 +79,7 @@ spotless { include '**/*.java' exclude '**/build/**', '**/build-*/**', '**/protobuf/**' } - licenseHeader('/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */') + licenseHeader('/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */') importOrder() removeUnusedImports() trimTrailingWhitespace() diff --git a/java/client/build.gradle b/java/client/build.gradle index 9fc4636bab..0178f311ea 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -2,6 +2,11 @@ import java.nio.file.Paths plugins { id 'java-library' + id 'maven-publish' + id 'signing' + id 'io.freefair.lombok' version '8.6' + id 'com.github.spotbugs' version '6.0.18' + id 'com.google.osdetector' version '1.7.3' } repositories { @@ -9,13 +14,14 @@ repositories { } dependencies { - implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '4.26.1' + implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '4.27.1' implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' implementation group: 'io.netty', name: 'netty-handler', version: '4.1.100.Final' // https://github.com/netty/netty/wiki/Native-transports // At the moment, Windows is not supported implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.100.Final', classifier: 'linux-x86_64' + implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.100.Final', classifier: 'linux-aarch_64' implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.100.Final', classifier: 'osx-x86_64' implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.100.Final', classifier: 'osx-aarch_64' @@ -67,7 +73,7 @@ tasks.register('protobuf', Exec) { '-Iprotobuf=glide-core/src/protobuf/', '--java_out=java/client/src/main/java/glide/models/protobuf', 'glide-core/src/protobuf/connection_request.proto', - 'glide-core/src/protobuf/redis_request.proto', + 'glide-core/src/protobuf/command_request.proto', 'glide-core/src/protobuf/response.proto' workingDir Paths.get(project.rootDir.path, '..').toFile() } @@ -138,8 +144,27 @@ tasks.register('buildAll') { finalizedBy 'build' } +tasks.register('buildAllRelease') { + dependsOn 'protobuf', 'buildRustRelease', 'testFfi' + finalizedBy 'build' +} + compileJava.dependsOn('protobuf') clean.dependsOn('cleanProtobuf', 'cleanRust') + +tasks.register('copyNativeLib', Copy) { + from "${projectDir}/../target/release" + include "*.dylib", "*.so" + into sourceSets.main.output.resourcesDir +} + +def defaultReleaseVersion = "255.255.255"; + +delombok.dependsOn('compileJava') +jar.dependsOn('copyNativeLib') +javadoc.dependsOn('copyNativeLib') +copyNativeLib.dependsOn('buildRustRelease') +compileTestJava.dependsOn('copyNativeLib') test.dependsOn('buildRust') testFfi.dependsOn('buildRust') @@ -147,15 +172,117 @@ test { exclude "glide/ffi/FfiTest.class" } +sourceSets { + main { + java { + srcDir 'src/main/java' + } + resources { + srcDir 'src/main/resources' + } + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + groupId = 'io.valkey' + artifactId = 'valkey-glide' + version = System.getenv("GLIDE_RELEASE_VERSION") ?: defaultReleaseVersion; + pom { + name = 'valkey-glide' + description = 'General Language Independent Driver for the Enterprise (GLIDE) for Valkey' + url = 'https://github.com/valkey-io/valkey-glide.git' + inceptionYear = '2024' + scm { + url = 'https://github.com/valkey-io/valkey-glide' + connection = 'scm:git:ssh://git@github.com/valkey-io/valkey-glide.git' + developerConnection = 'scm:git:ssh://git@github.com/valkey-io/valkey-glide.git' + } + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + developers { + developer { + name = 'valkey-glide' + url = 'https://github.com/valkey-io/valkey-glide.git' + } + } + } + } + } + repositories { + mavenLocal() + } +} + +java { + modularity.inferModulePath = true + withSourcesJar() + withJavadocJar() +} + +tasks.withType(Sign) { + def releaseVersion = System.getenv("GLIDE_RELEASE_VERSION") ?: defaultReleaseVersion; + def isReleaseVersion = !releaseVersion.endsWith("SNAPSHOT") && releaseVersion != defaultReleaseVersion; + onlyIf("isReleaseVersion is set") { isReleaseVersion } +} + +signing { + sign publishing.publications +} + tasks.withType(Test) { testLogging { exceptionFormat "full" events "started", "skipped", "passed", "failed" showStandardStreams true } + // This is needed for the FFI tests jvmArgs "-Djava.library.path=${projectDir}/../target/debug" } jar { - archiveBaseName = "glide" + archiveClassifier = osdetector.classifier +} + +sourcesJar { + // suppress following error + // Entry glide/api/BaseClient.java is a duplicate but no duplicate handling strategy has been set + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +delombok { + modulePath = classpath +} + +javadoc { + dependsOn delombok + source = delombok.outputs + options.tags = [ "example:a:Example:" ] + failOnError = false // TODO fix all javadoc errors and warnings and remove that +} + +spotbugsMain { + reports { + html { + required = true + outputLocation = file("$buildDir/reports/spotbugs/main/spotbugs.html") + stylesheet = 'fancy-hist.xsl' + } + xml { + required = true + outputLocation = file("$buildDir/reports/spotbugs/main/spotbugs.xml") + } + } +} + +spotbugs { + ignoreFailures = true + showStackTraces = true } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 809848fab8..fceaf82634 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1,139 +1,296 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static command_request.CommandRequestOuterClass.RequestType.Append; +import static command_request.CommandRequestOuterClass.RequestType.BLMPop; +import static command_request.CommandRequestOuterClass.RequestType.BLMove; +import static command_request.CommandRequestOuterClass.RequestType.BLPop; +import static command_request.CommandRequestOuterClass.RequestType.BRPop; +import static command_request.CommandRequestOuterClass.RequestType.BZMPop; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.BitCount; +import static command_request.CommandRequestOuterClass.RequestType.BitField; +import static command_request.CommandRequestOuterClass.RequestType.BitFieldReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.BitOp; +import static command_request.CommandRequestOuterClass.RequestType.BitPos; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.Decr; +import static command_request.CommandRequestOuterClass.RequestType.DecrBy; +import static command_request.CommandRequestOuterClass.RequestType.Del; +import static command_request.CommandRequestOuterClass.RequestType.Dump; +import static command_request.CommandRequestOuterClass.RequestType.Exists; +import static command_request.CommandRequestOuterClass.RequestType.Expire; +import static command_request.CommandRequestOuterClass.RequestType.ExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.ExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.FCall; +import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.GeoAdd; +import static command_request.CommandRequestOuterClass.RequestType.GeoDist; +import static command_request.CommandRequestOuterClass.RequestType.GeoHash; +import static command_request.CommandRequestOuterClass.RequestType.GeoPos; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearch; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearchStore; +import static command_request.CommandRequestOuterClass.RequestType.Get; +import static command_request.CommandRequestOuterClass.RequestType.GetBit; +import static command_request.CommandRequestOuterClass.RequestType.GetDel; +import static command_request.CommandRequestOuterClass.RequestType.GetEx; +import static command_request.CommandRequestOuterClass.RequestType.GetRange; +import static command_request.CommandRequestOuterClass.RequestType.HDel; +import static command_request.CommandRequestOuterClass.RequestType.HExists; +import static command_request.CommandRequestOuterClass.RequestType.HGet; +import static command_request.CommandRequestOuterClass.RequestType.HGetAll; +import static command_request.CommandRequestOuterClass.RequestType.HIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.HIncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.HKeys; +import static command_request.CommandRequestOuterClass.RequestType.HLen; +import static command_request.CommandRequestOuterClass.RequestType.HMGet; +import static command_request.CommandRequestOuterClass.RequestType.HRandField; +import static command_request.CommandRequestOuterClass.RequestType.HScan; +import static command_request.CommandRequestOuterClass.RequestType.HSet; +import static command_request.CommandRequestOuterClass.RequestType.HSetNX; +import static command_request.CommandRequestOuterClass.RequestType.HStrlen; +import static command_request.CommandRequestOuterClass.RequestType.HVals; +import static command_request.CommandRequestOuterClass.RequestType.Incr; +import static command_request.CommandRequestOuterClass.RequestType.IncrBy; +import static command_request.CommandRequestOuterClass.RequestType.IncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.LCS; +import static command_request.CommandRequestOuterClass.RequestType.LIndex; +import static command_request.CommandRequestOuterClass.RequestType.LInsert; +import static command_request.CommandRequestOuterClass.RequestType.LLen; +import static command_request.CommandRequestOuterClass.RequestType.LMPop; +import static command_request.CommandRequestOuterClass.RequestType.LMove; +import static command_request.CommandRequestOuterClass.RequestType.LPop; +import static command_request.CommandRequestOuterClass.RequestType.LPos; +import static command_request.CommandRequestOuterClass.RequestType.LPush; +import static command_request.CommandRequestOuterClass.RequestType.LPushX; +import static command_request.CommandRequestOuterClass.RequestType.LRange; +import static command_request.CommandRequestOuterClass.RequestType.LRem; +import static command_request.CommandRequestOuterClass.RequestType.LSet; +import static command_request.CommandRequestOuterClass.RequestType.LTrim; +import static command_request.CommandRequestOuterClass.RequestType.MGet; +import static command_request.CommandRequestOuterClass.RequestType.MSet; +import static command_request.CommandRequestOuterClass.RequestType.MSetNX; +import static command_request.CommandRequestOuterClass.RequestType.ObjectEncoding; +import static command_request.CommandRequestOuterClass.RequestType.ObjectFreq; +import static command_request.CommandRequestOuterClass.RequestType.ObjectIdleTime; +import static command_request.CommandRequestOuterClass.RequestType.ObjectRefCount; +import static command_request.CommandRequestOuterClass.RequestType.PExpire; +import static command_request.CommandRequestOuterClass.RequestType.PExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.PExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.PTTL; +import static command_request.CommandRequestOuterClass.RequestType.Persist; +import static command_request.CommandRequestOuterClass.RequestType.PfAdd; +import static command_request.CommandRequestOuterClass.RequestType.PfCount; +import static command_request.CommandRequestOuterClass.RequestType.PfMerge; +import static command_request.CommandRequestOuterClass.RequestType.Publish; +import static command_request.CommandRequestOuterClass.RequestType.RPop; +import static command_request.CommandRequestOuterClass.RequestType.RPush; +import static command_request.CommandRequestOuterClass.RequestType.RPushX; +import static command_request.CommandRequestOuterClass.RequestType.Rename; +import static command_request.CommandRequestOuterClass.RequestType.RenameNX; +import static command_request.CommandRequestOuterClass.RequestType.Restore; +import static command_request.CommandRequestOuterClass.RequestType.SAdd; +import static command_request.CommandRequestOuterClass.RequestType.SCard; +import static command_request.CommandRequestOuterClass.RequestType.SDiff; +import static command_request.CommandRequestOuterClass.RequestType.SDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.SInter; +import static command_request.CommandRequestOuterClass.RequestType.SInterCard; +import static command_request.CommandRequestOuterClass.RequestType.SInterStore; +import static command_request.CommandRequestOuterClass.RequestType.SIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMembers; +import static command_request.CommandRequestOuterClass.RequestType.SMove; +import static command_request.CommandRequestOuterClass.RequestType.SPop; +import static command_request.CommandRequestOuterClass.RequestType.SRandMember; +import static command_request.CommandRequestOuterClass.RequestType.SRem; +import static command_request.CommandRequestOuterClass.RequestType.SScan; +import static command_request.CommandRequestOuterClass.RequestType.SUnion; +import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; +import static command_request.CommandRequestOuterClass.RequestType.Set; +import static command_request.CommandRequestOuterClass.RequestType.SetBit; +import static command_request.CommandRequestOuterClass.RequestType.SetRange; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Strlen; +import static command_request.CommandRequestOuterClass.RequestType.TTL; +import static command_request.CommandRequestOuterClass.RequestType.Touch; +import static command_request.CommandRequestOuterClass.RequestType.Type; +import static command_request.CommandRequestOuterClass.RequestType.Unlink; +import static command_request.CommandRequestOuterClass.RequestType.Wait; +import static command_request.CommandRequestOuterClass.RequestType.Watch; +import static command_request.CommandRequestOuterClass.RequestType.XAck; +import static command_request.CommandRequestOuterClass.RequestType.XAdd; +import static command_request.CommandRequestOuterClass.RequestType.XAutoClaim; +import static command_request.CommandRequestOuterClass.RequestType.XClaim; +import static command_request.CommandRequestOuterClass.RequestType.XDel; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreate; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreateConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDelConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDestroy; +import static command_request.CommandRequestOuterClass.RequestType.XGroupSetId; +import static command_request.CommandRequestOuterClass.RequestType.XInfoConsumers; +import static command_request.CommandRequestOuterClass.RequestType.XInfoGroups; +import static command_request.CommandRequestOuterClass.RequestType.XInfoStream; +import static command_request.CommandRequestOuterClass.RequestType.XLen; +import static command_request.CommandRequestOuterClass.RequestType.XPending; +import static command_request.CommandRequestOuterClass.RequestType.XRange; +import static command_request.CommandRequestOuterClass.RequestType.XRead; +import static command_request.CommandRequestOuterClass.RequestType.XReadGroup; +import static command_request.CommandRequestOuterClass.RequestType.XRevRange; +import static command_request.CommandRequestOuterClass.RequestType.XTrim; +import static command_request.CommandRequestOuterClass.RequestType.ZAdd; +import static command_request.CommandRequestOuterClass.RequestType.ZCard; +import static command_request.CommandRequestOuterClass.RequestType.ZCount; +import static command_request.CommandRequestOuterClass.RequestType.ZDiff; +import static command_request.CommandRequestOuterClass.RequestType.ZDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.ZIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.ZInter; +import static command_request.CommandRequestOuterClass.RequestType.ZInterCard; +import static command_request.CommandRequestOuterClass.RequestType.ZInterStore; +import static command_request.CommandRequestOuterClass.RequestType.ZLexCount; +import static command_request.CommandRequestOuterClass.RequestType.ZMPop; +import static command_request.CommandRequestOuterClass.RequestType.ZMScore; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.ZRandMember; +import static command_request.CommandRequestOuterClass.RequestType.ZRange; +import static command_request.CommandRequestOuterClass.RequestType.ZRangeStore; +import static command_request.CommandRequestOuterClass.RequestType.ZRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRem; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByLex; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByScore; +import static command_request.CommandRequestOuterClass.RequestType.ZRevRank; +import static command_request.CommandRequestOuterClass.RequestType.ZScan; +import static command_request.CommandRequestOuterClass.RequestType.ZScore; +import static command_request.CommandRequestOuterClass.RequestType.ZUnion; +import static command_request.CommandRequestOuterClass.RequestType.ZUnionStore; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; +import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldGlideStringArgs; +import static glide.api.models.commands.stream.StreamClaimOptions.JUST_ID_VALKEY_API; +import static glide.api.models.commands.stream.StreamGroupOptions.ENTRIES_READ_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.XInfoStreamOptions.COUNT; +import static glide.api.models.commands.stream.XInfoStreamOptions.FULL; import static glide.ffi.resolvers.SocketListenerResolver.getSocket; +import static glide.utils.ArrayTransformUtils.cast3DArray; import static glide.utils.ArrayTransformUtils.castArray; +import static glide.utils.ArrayTransformUtils.castArrayofArrays; +import static glide.utils.ArrayTransformUtils.castBinaryStringMapOfArrays; +import static glide.utils.ArrayTransformUtils.castMapOf2DArray; +import static glide.utils.ArrayTransformUtils.castMapOfArrays; import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static glide.utils.ArrayTransformUtils.convertMapToKeyValueGlideStringArray; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; -import static redis_request.RedisRequestOuterClass.RequestType.Blpop; -import static redis_request.RedisRequestOuterClass.RequestType.Brpop; -import static redis_request.RedisRequestOuterClass.RequestType.Decr; -import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; -import static redis_request.RedisRequestOuterClass.RequestType.Del; -import static redis_request.RedisRequestOuterClass.RequestType.Exists; -import static redis_request.RedisRequestOuterClass.RequestType.Expire; -import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.GetRange; -import static redis_request.RedisRequestOuterClass.RequestType.GetString; -import static redis_request.RedisRequestOuterClass.RequestType.HLen; -import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; -import static redis_request.RedisRequestOuterClass.RequestType.HashDel; -import static redis_request.RedisRequestOuterClass.RequestType.HashExists; -import static redis_request.RedisRequestOuterClass.RequestType.HashGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashGetAll; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.HashMGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashSet; -import static redis_request.RedisRequestOuterClass.RequestType.Hvals; -import static redis_request.RedisRequestOuterClass.RequestType.Incr; -import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.LInsert; -import static redis_request.RedisRequestOuterClass.RequestType.LLen; -import static redis_request.RedisRequestOuterClass.RequestType.LPop; -import static redis_request.RedisRequestOuterClass.RequestType.LPush; -import static redis_request.RedisRequestOuterClass.RequestType.LPushX; -import static redis_request.RedisRequestOuterClass.RequestType.LRange; -import static redis_request.RedisRequestOuterClass.RequestType.LRem; -import static redis_request.RedisRequestOuterClass.RequestType.LTrim; -import static redis_request.RedisRequestOuterClass.RequestType.Lindex; -import static redis_request.RedisRequestOuterClass.RequestType.MGet; -import static redis_request.RedisRequestOuterClass.RequestType.MSet; -import static redis_request.RedisRequestOuterClass.RequestType.PExpire; -import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.PTTL; -import static redis_request.RedisRequestOuterClass.RequestType.Persist; -import static redis_request.RedisRequestOuterClass.RequestType.PfAdd; -import static redis_request.RedisRequestOuterClass.RequestType.PfCount; -import static redis_request.RedisRequestOuterClass.RequestType.PfMerge; -import static redis_request.RedisRequestOuterClass.RequestType.RPop; -import static redis_request.RedisRequestOuterClass.RequestType.RPush; -import static redis_request.RedisRequestOuterClass.RequestType.RPushX; -import static redis_request.RedisRequestOuterClass.RequestType.SAdd; -import static redis_request.RedisRequestOuterClass.RequestType.SCard; -import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.SInter; -import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; -import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMembers; -import static redis_request.RedisRequestOuterClass.RequestType.SMove; -import static redis_request.RedisRequestOuterClass.RequestType.SRem; -import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; -import static redis_request.RedisRequestOuterClass.RequestType.SetRange; -import static redis_request.RedisRequestOuterClass.RequestType.SetString; -import static redis_request.RedisRequestOuterClass.RequestType.Strlen; -import static redis_request.RedisRequestOuterClass.RequestType.TTL; -import static redis_request.RedisRequestOuterClass.RequestType.Type; -import static redis_request.RedisRequestOuterClass.RequestType.Unlink; -import static redis_request.RedisRequestOuterClass.RequestType.XAdd; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount; -import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; -import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZScore; -import static redis_request.RedisRequestOuterClass.RequestType.Zadd; -import static redis_request.RedisRequestOuterClass.RequestType.Zcard; -import static redis_request.RedisRequestOuterClass.RequestType.Zcount; -import static redis_request.RedisRequestOuterClass.RequestType.Zrange; -import static redis_request.RedisRequestOuterClass.RequestType.Zrank; -import static redis_request.RedisRequestOuterClass.RequestType.Zrem; +import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArrayBinary; +import static glide.utils.ArrayTransformUtils.mapGeoDataToArray; +import static glide.utils.ArrayTransformUtils.mapGeoDataToGlideStringArray; +import glide.api.commands.BitmapBaseCommands; import glide.api.commands.GenericBaseCommands; +import glide.api.commands.GeospatialIndicesBaseCommands; import glide.api.commands.HashBaseCommands; import glide.api.commands.HyperLogLogBaseCommands; import glide.api.commands.ListBaseCommands; +import glide.api.commands.PubSubBaseCommands; +import glide.api.commands.ScriptingAndFunctionsBaseCommands; import glide.api.commands.SetBaseCommands; import glide.api.commands.SortedSetBaseCommands; import glide.api.commands.StreamBaseCommands; import glide.api.commands.StringBaseCommands; +import glide.api.commands.TransactionsBaseCommands; +import glide.api.models.GlideString; +import glide.api.models.PubSubMessage; import glide.api.models.Script; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.GetExOptions; import glide.api.models.commands.LInsertOptions.InsertPosition; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.LexRange; import glide.api.models.commands.RangeOptions.RangeQuery; import glide.api.models.commands.RangeOptions.ScoreRange; import glide.api.models.commands.RangeOptions.ScoredRangeQuery; +import glide.api.models.commands.RestoreOptions; +import glide.api.models.commands.ScoreFilter; import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.SetOptions; -import glide.api.models.commands.StreamAddOptions; -import glide.api.models.commands.ZaddOptions; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.WeightAggregateOptions.KeyArrayBinary; +import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; +import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeysBinary; +import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.geospatial.GeoAddOptions; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.HScanOptionsBinary; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.SScanOptionsBinary; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.scan.ZScanOptionsBinary; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamAddOptionsBinary; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamPendingOptions; +import glide.api.models.commands.stream.StreamPendingOptionsBinary; +import glide.api.models.commands.stream.StreamRange; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions; import glide.api.models.configuration.BaseClientConfiguration; -import glide.api.models.exceptions.RedisException; +import glide.api.models.configuration.BaseSubscriptionConfiguration; +import glide.api.models.exceptions.ConfigurationError; +import glide.api.models.exceptions.GlideException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; +import glide.connectors.handlers.MessageHandler; import glide.connectors.resources.Platform; import glide.connectors.resources.ThreadPoolResource; import glide.connectors.resources.ThreadPoolResourceAllocator; -import glide.ffi.resolvers.RedisValueResolver; -import glide.managers.BaseCommandResponseResolver; +import glide.ffi.resolvers.GlideValueResolver; +import glide.managers.BaseResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import glide.utils.ArgsBuilder; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.function.BiFunction; -import lombok.AllArgsConstructor; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.NonNull; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.ArrayUtils; import response.ResponseOuterClass.ConstantResponse; import response.ResponseOuterClass.Response; -/** Base Client class for Redis */ -@AllArgsConstructor +/** Base Client class */ public abstract class BaseClient implements AutoCloseable, + BitmapBaseCommands, GenericBaseCommands, StringBaseCommands, HashBaseCommands, @@ -141,38 +298,76 @@ public abstract class BaseClient SetBaseCommands, SortedSetBaseCommands, StreamBaseCommands, - HyperLogLogBaseCommands { + HyperLogLogBaseCommands, + GeospatialIndicesBaseCommands, + ScriptingAndFunctionsBaseCommands, + TransactionsBaseCommands, + PubSubBaseCommands { - /** Redis simple string response with "OK" */ + /** Valkey simple string response with "OK" */ public static final String OK = ConstantResponse.OK.toString(); - protected final ConnectionManager connectionManager; protected final CommandManager commandManager; + protected final ConnectionManager connectionManager; + protected final MessageHandler messageHandler; + protected final Optional subscriptionConfiguration; + + /** Helper which extracts data from received {@link Response}s from GLIDE. */ + private static final BaseResponseResolver responseResolver = + new BaseResponseResolver(GlideValueResolver::valueFromPointer); + + /** Helper which extracts data with binary strings from received {@link Response}s from GLIDE. */ + private static final BaseResponseResolver binaryResponseResolver = + new BaseResponseResolver(GlideValueResolver::valueFromPointerBinary); + + /** A constructor. */ + protected BaseClient(ClientBuilder builder) { + this.connectionManager = builder.connectionManager; + this.commandManager = builder.commandManager; + this.messageHandler = builder.messageHandler; + this.subscriptionConfiguration = builder.subscriptionConfiguration; + } + + /** Auxiliary builder which wraps all fields to be initialized in the constructor. */ + @RequiredArgsConstructor + protected static class ClientBuilder { + private final ConnectionManager connectionManager; + private final CommandManager commandManager; + private final MessageHandler messageHandler; + private final Optional subscriptionConfiguration; + } /** - * Async request for an async (non-blocking) Redis client. + * Async request for an async (non-blocking) client. * - * @param config Redis client Configuration. - * @param constructor Redis client constructor reference. + * @param config client Configuration. + * @param constructor client constructor reference. * @param Client type. - * @return a Future to connect and return a RedisClient. + * @return a Future to connect and return a client. */ - protected static CompletableFuture CreateClient( - BaseClientConfiguration config, - BiFunction constructor) { + protected static CompletableFuture createClient( + @NonNull BaseClientConfiguration config, Function constructor) { try { ThreadPoolResource threadPoolResource = config.getThreadPoolResource(); if (threadPoolResource == null) { threadPoolResource = ThreadPoolResourceAllocator.getOrCreate(Platform.getThreadPoolResourceSupplier()); } - ChannelHandler channelHandler = buildChannelHandler(threadPoolResource); + MessageHandler messageHandler = buildMessageHandler(config); + ChannelHandler channelHandler = buildChannelHandler(threadPoolResource, messageHandler); ConnectionManager connectionManager = buildConnectionManager(channelHandler); CommandManager commandManager = buildCommandManager(channelHandler); // TODO: Support exception throwing, including interrupted exceptions return connectionManager - .connectToRedis(config) - .thenApply(ignore -> constructor.apply(connectionManager, commandManager)); + .connectToValkey(config) + .thenApply( + ignored -> + constructor.apply( + new ClientBuilder( + connectionManager, + commandManager, + messageHandler, + Optional.ofNullable(config.getSubscriptionConfiguration())))); } catch (InterruptedException e) { // Something bad happened while we were establishing netty connection to UDS var future = new CompletableFuture(); @@ -181,6 +376,49 @@ protected static CompletableFuture CreateClient( } } + /** + * Return a next pubsub message if it is present. + * + * @throws ConfigurationError If client is not subscribed to any channel or if client configured + * with a callback. + * @return A message if any or null if there are no unread messages. + */ + public PubSubMessage tryGetPubSubMessage() { + if (subscriptionConfiguration.isEmpty()) { + throw new ConfigurationError( + "The operation will never complete since there was no pubsub subscriptions applied to the" + + " client."); + } + if (subscriptionConfiguration.get().getCallback().isPresent()) { + throw new ConfigurationError( + "The operation will never complete since messages will be passed to the configured" + + " callback."); + } + return messageHandler.getQueue().popSync(); + } + + /** + * Returns a promise for a next pubsub message.
+ * Message gets unrecoverable lost if future is cancelled or reference to this future is lost. + * + * @throws ConfigurationError If client is not subscribed to any channel or if client configured + * with a callback. + * @return A {@link CompletableFuture} which will asynchronously hold the next available message. + */ + public CompletableFuture getPubSubMessage() { + if (subscriptionConfiguration.isEmpty()) { + throw new ConfigurationError( + "The operation will never complete since there was no pubsub subscriptions applied to the" + + " client."); + } + if (subscriptionConfiguration.get().getCallback().isPresent()) { + throw new ConfigurationError( + "The operation will never complete since messages will be passed to the configured" + + " callback."); + } + return messageHandler.getQueue().popAsync(); + } + /** * Closes this resource, relinquishing any underlying resources. This method is invoked * automatically on objects managed by the try-with-resources statement. @@ -198,9 +436,20 @@ public void close() throws ExecutionException { } } - protected static ChannelHandler buildChannelHandler(ThreadPoolResource threadPoolResource) + protected static MessageHandler buildMessageHandler(BaseClientConfiguration config) { + if (config.getSubscriptionConfiguration() == null) { + return new MessageHandler(Optional.empty(), Optional.empty(), binaryResponseResolver); + } + return new MessageHandler( + config.getSubscriptionConfiguration().getCallback(), + config.getSubscriptionConfiguration().getContext(), + binaryResponseResolver); + } + + protected static ChannelHandler buildChannelHandler( + ThreadPoolResource threadPoolResource, MessageHandler messageHandler) throws InterruptedException { - CallbackDispatcher callbackDispatcher = new CallbackDispatcher(); + CallbackDispatcher callbackDispatcher = new CallbackDispatcher(messageHandler); return new ChannelHandler(callbackDispatcher, getSocket(), threadPoolResource); } @@ -214,73 +463,117 @@ protected static CommandManager buildCommandManager(ChannelHandler channelHandle /** * Extracts the value from a GLIDE core response message and either throws an - * exception or returns the value as an object of type T. If isNullable, - * than also returns null. + * exception or returns the value as an object of type T. * - * @param response Redis protobuf message. + * @param response protobuf message. * @param classType Parameter T class type. - * @param isNullable Accepts null values in the protobuf message. + * @param flags A set of parameters which describes how to handle the response. Could be empty or + * any combination of + *
    + *
  • {@link ResponseFlags#ENCODING_UTF8} to return the data as a String; if + * unset, a byte[] is returned. + *
  • {@link ResponseFlags#IS_NULLABLE} to accept null values. + *
+ * * @return Response as an object of type T or null. * @param The return value type. - * @throws RedisException On a type mismatch. + * @throws GlideException On a type mismatch. */ @SuppressWarnings("unchecked") - protected T handleRedisResponse(Class classType, boolean isNullable, Response response) - throws RedisException { + protected T handleValkeyResponse( + Class classType, EnumSet flags, Response response) throws GlideException { + boolean encodingUtf8 = flags.contains(ResponseFlags.ENCODING_UTF8); + boolean isNullable = flags.contains(ResponseFlags.IS_NULLABLE); Object value = - new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response); + encodingUtf8 ? responseResolver.apply(response) : binaryResponseResolver.apply(response); if (isNullable && (value == null)) { return null; } + + value = convertByteArrayToGlideString(value); + if (classType.isInstance(value)) { return (T) value; } String className = value == null ? "null" : value.getClass().getSimpleName(); - throw new RedisException( - "Unexpected return type from Redis: got " + throw new GlideException( + "Unexpected return type from Glide: got " + className + " expected " + classType.getSimpleName()); } - protected Object handleObjectOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Object.class, true, response); + protected Object handleObjectOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse( + Object.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); + } + + protected Object handleBinaryObjectOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse(Object.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); + } + + protected String handleStringResponse(Response response) throws GlideException { + return handleValkeyResponse(String.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); + } + + protected String handleStringOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse( + String.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); + } + + protected byte[] handleBytesOrNullResponse(Response response) throws GlideException { + var result = + handleValkeyResponse(GlideString.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); + if (result == null) return null; + + return result.getBytes(); + } + + protected GlideString handleGlideStringOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse(GlideString.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } - protected String handleStringResponse(Response response) throws RedisException { - return handleRedisResponse(String.class, false, response); + protected GlideString handleGlideStringResponse(Response response) throws GlideException { + return handleValkeyResponse(GlideString.class, EnumSet.noneOf(ResponseFlags.class), response); } - protected String handleStringOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(String.class, true, response); + protected Boolean handleBooleanResponse(Response response) throws GlideException { + return handleValkeyResponse(Boolean.class, EnumSet.noneOf(ResponseFlags.class), response); } - protected Boolean handleBooleanResponse(Response response) throws RedisException { - return handleRedisResponse(Boolean.class, false, response); + protected Long handleLongResponse(Response response) throws GlideException { + return handleValkeyResponse(Long.class, EnumSet.noneOf(ResponseFlags.class), response); } - protected Long handleLongResponse(Response response) throws RedisException { - return handleRedisResponse(Long.class, false, response); + protected Long handleLongOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse(Long.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } - protected Long handleLongOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Long.class, true, response); + protected Double handleDoubleResponse(Response response) throws GlideException { + return handleValkeyResponse(Double.class, EnumSet.noneOf(ResponseFlags.class), response); } - protected Double handleDoubleResponse(Response response) throws RedisException { - return handleRedisResponse(Double.class, false, response); + protected Double handleDoubleOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse(Double.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } - protected Double handleDoubleOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Double.class, true, response); + protected Object[] handleArrayResponse(Response response) throws GlideException { + return handleValkeyResponse(Object[].class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } - protected Object[] handleArrayResponse(Response response) throws RedisException { - return handleRedisResponse(Object[].class, false, response); + protected Object[] handleArrayResponseBinary(Response response) throws GlideException { + return handleValkeyResponse(Object[].class, EnumSet.noneOf(ResponseFlags.class), response); } - protected Object[] handleArrayOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Object[].class, true, response); + protected Object[] handleArrayOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse( + Object[].class, + EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), + response); + } + + protected Object[] handleArrayOrNullResponseBinary(Response response) throws GlideException { + return handleValkeyResponse(Object[].class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } /** @@ -289,13 +582,150 @@ protected Object[] handleArrayOrNullResponse(Response response) throws RedisExce * @param Value type. */ @SuppressWarnings("unchecked") // raw Map cast to Map - protected Map handleMapResponse(Response response) throws RedisException { - return handleRedisResponse(Map.class, false, response); + protected Map handleMapResponse(Response response) throws GlideException { + return handleValkeyResponse(Map.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); + } + + /** + * Get a map and convert {@link Map} keys from byte[] to {@link String}. + * + * @param response A Protobuf response + * @return A map of GlideString to V. + * @param Value type. + */ + @SuppressWarnings("unchecked") // raw Map cast to Map + protected Map handleBinaryStringMapResponse(Response response) + throws GlideException { + return handleValkeyResponse(Map.class, EnumSet.noneOf(ResponseFlags.class), response); + } + + /** + * @param response A Protobuf response + * @return A map of String to V or null + * @param Value type. + */ + @SuppressWarnings("unchecked") // raw Map cast to Map + protected Map handleMapOrNullResponse(Response response) throws GlideException { + return handleValkeyResponse( + Map.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); + } + + /** + * @param response A Protobuf response + * @return A map of String to V or null + * @param Value type. + */ + @SuppressWarnings("unchecked") // raw Map cast to Map + protected Map handleBinaryStringMapOrNullResponse(Response response) + throws GlideException { + return handleValkeyResponse(Map.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); + } + + /** + * @param response A Protobuf response + * @return A map of a map of String[][] + */ + protected Map> handleXReadResponse(Response response) + throws GlideException { + Map mapResponse = handleMapOrNullResponse(response); + if (mapResponse == null) { + return null; + } + return mapResponse.entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + e -> castMapOf2DArray((Map) e.getValue(), String.class))); + } + + /** + * @param response A Protobuf response + * @return A map of a map of GlideString[][] + */ + protected Map> handleXReadResponseBinary( + Response response) throws GlideException { + Map mapResponse = handleBinaryStringMapOrNullResponse(response); + if (mapResponse == null) { + return null; + } + return mapResponse.entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + e -> + castMapOf2DArray( + (Map) e.getValue(), GlideString.class))); } @SuppressWarnings("unchecked") // raw Set cast to Set - protected Set handleSetResponse(Response response) throws RedisException { - return handleRedisResponse(Set.class, false, response); + protected Set handleSetResponse(Response response) throws GlideException { + return handleValkeyResponse(Set.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); + } + + @SuppressWarnings("unchecked") + protected Set handleSetBinaryResponse(Response response) throws GlideException { + return handleValkeyResponse(Set.class, EnumSet.noneOf(ResponseFlags.class), response); + } + + /** Process a FUNCTION LIST standalone response. */ + @SuppressWarnings("unchecked") + protected Map[] handleFunctionListResponse(Object[] response) { + Map[] data = castArray(response, Map.class); + for (Map libraryInfo : data) { + Object[] functions = (Object[]) libraryInfo.get("functions"); + var functionInfo = castArray(functions, Map.class); + libraryInfo.put("functions", functionInfo); + } + return data; + } + + /** Process a FUNCTION LIST standalone response. */ + @SuppressWarnings("unchecked") + protected Map[] handleFunctionListResponseBinary(Object[] response) { + Map[] data = castArray(response, Map.class); + for (Map libraryInfo : data) { + Object[] functions = (Object[]) libraryInfo.get(gs("functions")); + var functionInfo = castArray(functions, Map.class); + libraryInfo.put(gs("functions"), functionInfo); + } + return data; + } + + /** Process a FUNCTION STATS standalone response. */ + protected Map> handleFunctionStatsResponse( + Map> response) { + Map runningScriptInfo = response.get("running_script"); + if (runningScriptInfo != null) { + Object[] command = (Object[]) runningScriptInfo.get("command"); + runningScriptInfo.put("command", castArray(command, String.class)); + } + return response; + } + + /** Process a FUNCTION STATS standalone response. */ + protected Map> handleFunctionStatsBinaryResponse( + Map> response) { + Map runningScriptInfo = response.get(gs("running_script")); + if (runningScriptInfo != null) { + Object[] command = (Object[]) runningScriptInfo.get(gs("command")); + runningScriptInfo.put(gs("command"), castArray(command, GlideString.class)); + } + return response; + } + + /** Process a LCS key1 key2 IDX response */ + protected Map handleLcsIdxResponse(Map response) + throws GlideException { + Long[][][] convertedMatchesObject = + cast3DArray((Object[]) (response.get(LCS_MATCHES_RESULT_KEY)), Long.class); + + if (convertedMatchesObject == null) { + throw new NullPointerException( + "LCS result does not contain the key \"" + LCS_MATCHES_RESULT_KEY + "\""); + } + + response.put("matches", convertedMatchesObject); + return response; } @Override @@ -304,294 +734,897 @@ public CompletableFuture del(@NonNull String[] keys) { } @Override - public CompletableFuture get(@NonNull String key) { - return commandManager.submitNewCommand( - GetString, new String[] {key}, this::handleStringOrNullResponse); + public CompletableFuture del(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(Del, keys, this::handleLongResponse); } @Override - public CompletableFuture set(@NonNull String key, @NonNull String value) { + public CompletableFuture get(@NonNull String key) { return commandManager.submitNewCommand( - SetString, new String[] {key, value}, this::handleStringResponse); + Get, new String[] {key}, this::handleStringOrNullResponse); } @Override - public CompletableFuture set( - @NonNull String key, @NonNull String value, @NonNull SetOptions options) { - String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); - return commandManager.submitNewCommand(SetString, arguments, this::handleStringOrNullResponse); + public CompletableFuture get(@NonNull GlideString key) { + return commandManager.submitNewCommand( + Get, new GlideString[] {key}, this::handleGlideStringOrNullResponse); } @Override - public CompletableFuture mget(@NonNull String[] keys) { + public CompletableFuture getdel(@NonNull String key) { return commandManager.submitNewCommand( - MGet, keys, response -> castArray(handleArrayOrNullResponse(response), String.class)); + GetDel, new String[] {key}, this::handleStringOrNullResponse); } @Override - public CompletableFuture mset(@NonNull Map keyValueMap) { - String[] args = convertMapToKeyValueStringArray(keyValueMap); - return commandManager.submitNewCommand(MSet, args, this::handleStringResponse); + public CompletableFuture getdel(@NonNull GlideString key) { + return commandManager.submitNewCommand( + GetDel, new GlideString[] {key}, this::handleGlideStringOrNullResponse); } @Override - public CompletableFuture incr(@NonNull String key) { - return commandManager.submitNewCommand(Incr, new String[] {key}, this::handleLongResponse); + public CompletableFuture getex(@NonNull String key) { + return commandManager.submitNewCommand( + GetEx, new String[] {key}, this::handleStringOrNullResponse); } @Override - public CompletableFuture incrBy(@NonNull String key, long amount) { + public CompletableFuture getex(@NonNull GlideString key) { return commandManager.submitNewCommand( - IncrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); + GetEx, new GlideString[] {key}, this::handleGlideStringOrNullResponse); } @Override - public CompletableFuture incrByFloat(@NonNull String key, double amount) { - return commandManager.submitNewCommand( - IncrByFloat, new String[] {key, Double.toString(amount)}, this::handleDoubleResponse); + public CompletableFuture getex(@NonNull String key, @NonNull GetExOptions options) { + String[] arguments = ArrayUtils.addFirst(options.toArgs(), key); + return commandManager.submitNewCommand(GetEx, arguments, this::handleStringOrNullResponse); } @Override - public CompletableFuture decr(@NonNull String key) { - return commandManager.submitNewCommand(Decr, new String[] {key}, this::handleLongResponse); + public CompletableFuture getex( + @NonNull GlideString key, @NonNull GetExOptions options) { + GlideString[] arguments = new ArgsBuilder().add(key).add(options.toArgs()).toArray(); + return commandManager.submitNewCommand(GetEx, arguments, this::handleGlideStringOrNullResponse); } @Override - public CompletableFuture decrBy(@NonNull String key, long amount) { + public CompletableFuture set(@NonNull String key, @NonNull String value) { return commandManager.submitNewCommand( - DecrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); + Set, new String[] {key, value}, this::handleStringResponse); } @Override - public CompletableFuture strlen(@NonNull String key) { - return commandManager.submitNewCommand(Strlen, new String[] {key}, this::handleLongResponse); + public CompletableFuture set(@NonNull GlideString key, @NonNull GlideString value) { + return commandManager.submitNewCommand( + Set, new GlideString[] {key, value}, this::handleStringResponse); } @Override - public CompletableFuture setrange(@NonNull String key, int offset, @NonNull String value) { - String[] arguments = new String[] {key, Integer.toString(offset), value}; - return commandManager.submitNewCommand(SetRange, arguments, this::handleLongResponse); + public CompletableFuture set( + @NonNull String key, @NonNull String value, @NonNull SetOptions options) { + String[] arguments = ArrayUtils.addAll(new String[] {key, value}, options.toArgs()); + return commandManager.submitNewCommand(Set, arguments, this::handleStringOrNullResponse); } @Override - public CompletableFuture getrange(@NonNull String key, int start, int end) { - String[] arguments = new String[] {key, Integer.toString(start), Integer.toString(end)}; - return commandManager.submitNewCommand(GetRange, arguments, this::handleStringResponse); + public CompletableFuture set( + @NonNull GlideString key, @NonNull GlideString value, @NonNull SetOptions options) { + GlideString[] arguments = new ArgsBuilder().add(key).add(value).add(options.toArgs()).toArray(); + return commandManager.submitNewCommand(Set, arguments, this::handleStringOrNullResponse); } @Override - public CompletableFuture hget(@NonNull String key, @NonNull String field) { + public CompletableFuture append(@NonNull String key, @NonNull String value) { return commandManager.submitNewCommand( - HashGet, new String[] {key, field}, this::handleStringOrNullResponse); + Append, new String[] {key, value}, this::handleLongResponse); } @Override - public CompletableFuture hset( - @NonNull String key, @NonNull Map fieldValueMap) { - String[] args = ArrayUtils.addFirst(convertMapToKeyValueStringArray(fieldValueMap), key); - return commandManager.submitNewCommand(HashSet, args, this::handleLongResponse); + public CompletableFuture append(@NonNull GlideString key, @NonNull GlideString value) { + return commandManager.submitNewCommand( + Append, new GlideString[] {key, value}, this::handleLongResponse); } @Override - public CompletableFuture hsetnx( - @NonNull String key, @NonNull String field, @NonNull String value) { + public CompletableFuture mget(@NonNull String[] keys) { return commandManager.submitNewCommand( - HSetNX, new String[] {key, field, value}, this::handleBooleanResponse); + MGet, keys, response -> castArray(handleArrayOrNullResponse(response), String.class)); } @Override - public CompletableFuture hdel(@NonNull String key, @NonNull String[] fields) { - String[] args = ArrayUtils.addFirst(fields, key); - return commandManager.submitNewCommand(HashDel, args, this::handleLongResponse); + public CompletableFuture mget(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand( + MGet, + keys, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override - public CompletableFuture hlen(@NonNull String key) { - return commandManager.submitNewCommand(HLen, new String[] {key}, this::handleLongResponse); + public CompletableFuture mset(@NonNull Map keyValueMap) { + String[] args = convertMapToKeyValueStringArray(keyValueMap); + return commandManager.submitNewCommand(MSet, args, this::handleStringResponse); } @Override - public CompletableFuture hvals(@NonNull String key) { - return commandManager.submitNewCommand( - Hvals, - new String[] {key}, - response -> castArray(handleArrayResponse(response), String.class)); + public CompletableFuture msetBinary(@NonNull Map keyValueMap) { + GlideString[] args = convertMapToKeyValueGlideStringArray(keyValueMap); + return commandManager.submitNewCommand(MSet, args, this::handleStringResponse); } @Override - public CompletableFuture hmget(@NonNull String key, @NonNull String[] fields) { - String[] arguments = ArrayUtils.addFirst(fields, key); + public CompletableFuture objectEncoding(@NonNull String key) { return commandManager.submitNewCommand( - HashMGet, arguments, response -> castArray(handleArrayResponse(response), String.class)); + ObjectEncoding, new String[] {key}, this::handleStringOrNullResponse); } @Override - public CompletableFuture hexists(@NonNull String key, @NonNull String field) { + public CompletableFuture objectEncoding(@NonNull GlideString key) { return commandManager.submitNewCommand( - HashExists, new String[] {key, field}, this::handleBooleanResponse); + ObjectEncoding, new GlideString[] {key}, this::handleStringOrNullResponse); } @Override - public CompletableFuture> hgetall(@NonNull String key) { - return commandManager.submitNewCommand(HashGetAll, new String[] {key}, this::handleMapResponse); + public CompletableFuture objectFreq(@NonNull String key) { + return commandManager.submitNewCommand( + ObjectFreq, new String[] {key}, this::handleLongOrNullResponse); } @Override - public CompletableFuture hincrBy(@NonNull String key, @NonNull String field, long amount) { + public CompletableFuture objectFreq(@NonNull GlideString key) { return commandManager.submitNewCommand( - HashIncrBy, new String[] {key, field, Long.toString(amount)}, this::handleLongResponse); + ObjectFreq, new GlideString[] {key}, this::handleLongOrNullResponse); } @Override - public CompletableFuture hincrByFloat( - @NonNull String key, @NonNull String field, double amount) { + public CompletableFuture objectIdletime(@NonNull String key) { return commandManager.submitNewCommand( - HashIncrByFloat, - new String[] {key, field, Double.toString(amount)}, - this::handleDoubleResponse); + ObjectIdleTime, new String[] {key}, this::handleLongOrNullResponse); } @Override - public CompletableFuture lpush(@NonNull String key, @NonNull String[] elements) { - String[] arguments = ArrayUtils.addFirst(elements, key); - return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); + public CompletableFuture objectIdletime(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ObjectIdleTime, new GlideString[] {key}, this::handleLongOrNullResponse); } @Override - public CompletableFuture lpop(@NonNull String key) { + public CompletableFuture objectRefcount(@NonNull String key) { return commandManager.submitNewCommand( - LPop, new String[] {key}, this::handleStringOrNullResponse); + ObjectRefCount, new String[] {key}, this::handleLongOrNullResponse); } @Override - public CompletableFuture lpopCount(@NonNull String key, long count) { + public CompletableFuture objectRefcount(@NonNull GlideString key) { return commandManager.submitNewCommand( - LPop, - new String[] {key, Long.toString(count)}, - response -> castArray(handleArrayResponse(response), String.class)); + ObjectRefCount, new GlideString[] {key}, this::handleLongOrNullResponse); } @Override - public CompletableFuture lrange(@NonNull String key, long start, long end) { + public CompletableFuture rename(@NonNull String key, @NonNull String newKey) { return commandManager.submitNewCommand( - LRange, - new String[] {key, Long.toString(start), Long.toString(end)}, - response -> castArray(handleArrayOrNullResponse(response), String.class)); + Rename, new String[] {key, newKey}, this::handleStringResponse); } @Override - public CompletableFuture lindex(@NonNull String key, long index) { + public CompletableFuture rename(@NonNull GlideString key, @NonNull GlideString newKey) { return commandManager.submitNewCommand( - Lindex, new String[] {key, Long.toString(index)}, this::handleStringOrNullResponse); + Rename, new GlideString[] {key, newKey}, this::handleStringResponse); } @Override - public CompletableFuture ltrim(@NonNull String key, long start, long end) { + public CompletableFuture renamenx(@NonNull String key, @NonNull String newKey) { return commandManager.submitNewCommand( - LTrim, - new String[] {key, Long.toString(start), Long.toString(end)}, - this::handleStringResponse); + RenameNX, new String[] {key, newKey}, this::handleBooleanResponse); } @Override - public CompletableFuture llen(@NonNull String key) { - return commandManager.submitNewCommand(LLen, new String[] {key}, this::handleLongResponse); + public CompletableFuture renamenx( + @NonNull GlideString key, @NonNull GlideString newKey) { + return commandManager.submitNewCommand( + RenameNX, new GlideString[] {key, newKey}, this::handleBooleanResponse); } @Override - public CompletableFuture lrem(@NonNull String key, long count, @NonNull String element) { - return commandManager.submitNewCommand( - LRem, new String[] {key, Long.toString(count), element}, this::handleLongResponse); + public CompletableFuture incr(@NonNull String key) { + return commandManager.submitNewCommand(Incr, new String[] {key}, this::handleLongResponse); } @Override - public CompletableFuture rpush(@NonNull String key, @NonNull String[] elements) { - String[] arguments = ArrayUtils.addFirst(elements, key); - return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); + public CompletableFuture incr(@NonNull GlideString key) { + return commandManager.submitNewCommand(Incr, new GlideString[] {key}, this::handleLongResponse); } @Override - public CompletableFuture rpop(@NonNull String key) { + public CompletableFuture incrBy(@NonNull String key, long amount) { return commandManager.submitNewCommand( - RPop, new String[] {key}, this::handleStringOrNullResponse); + IncrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); } @Override - public CompletableFuture rpopCount(@NonNull String key, long count) { + public CompletableFuture incrBy(@NonNull GlideString key, long amount) { return commandManager.submitNewCommand( - RPop, - new String[] {key, Long.toString(count)}, - response -> castArray(handleArrayOrNullResponse(response), String.class)); + IncrBy, new GlideString[] {key, gs(Long.toString(amount))}, this::handleLongResponse); } @Override - public CompletableFuture sadd(@NonNull String key, @NonNull String[] members) { - String[] arguments = ArrayUtils.addFirst(members, key); - return commandManager.submitNewCommand(SAdd, arguments, this::handleLongResponse); + public CompletableFuture incrByFloat(@NonNull String key, double amount) { + return commandManager.submitNewCommand( + IncrByFloat, new String[] {key, Double.toString(amount)}, this::handleDoubleResponse); } @Override - public CompletableFuture sismember(@NonNull String key, @NonNull String member) { + public CompletableFuture incrByFloat(@NonNull GlideString key, double amount) { return commandManager.submitNewCommand( - SIsMember, new String[] {key, member}, this::handleBooleanResponse); + IncrByFloat, + new GlideString[] {key, gs(Double.toString(amount))}, + this::handleDoubleResponse); } @Override - public CompletableFuture srem(@NonNull String key, @NonNull String[] members) { - String[] arguments = ArrayUtils.addFirst(members, key); - return commandManager.submitNewCommand(SRem, arguments, this::handleLongResponse); + public CompletableFuture decr(@NonNull String key) { + return commandManager.submitNewCommand(Decr, new String[] {key}, this::handleLongResponse); } @Override - public CompletableFuture> smembers(@NonNull String key) { - return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); + public CompletableFuture decr(@NonNull GlideString key) { + return commandManager.submitNewCommand(Decr, new GlideString[] {key}, this::handleLongResponse); } @Override - public CompletableFuture scard(@NonNull String key) { - return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); + public CompletableFuture decrBy(@NonNull String key, long amount) { + return commandManager.submitNewCommand( + DecrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); } @Override - public CompletableFuture smismember(@NonNull String key, @NonNull String[] members) { - String[] arguments = ArrayUtils.addFirst(members, key); + public CompletableFuture decrBy(@NonNull GlideString key, long amount) { return commandManager.submitNewCommand( - SMIsMember, arguments, response -> castArray(handleArrayResponse(response), Boolean.class)); + DecrBy, new GlideString[] {key, gs(Long.toString(amount))}, this::handleLongResponse); } @Override - public CompletableFuture sdiffstore(@NonNull String destination, @NonNull String[] keys) { - String[] arguments = ArrayUtils.addFirst(keys, destination); - return commandManager.submitNewCommand(SDiffStore, arguments, this::handleLongResponse); + public CompletableFuture strlen(@NonNull String key) { + return commandManager.submitNewCommand(Strlen, new String[] {key}, this::handleLongResponse); } @Override - public CompletableFuture smove( - @NonNull String source, @NonNull String destination, @NonNull String member) { + public CompletableFuture strlen(@NonNull GlideString key) { return commandManager.submitNewCommand( - SMove, new String[] {source, destination, member}, this::handleBooleanResponse); + Strlen, new GlideString[] {key}, this::handleLongResponse); } @Override - public CompletableFuture sinterstore(@NonNull String destination, @NonNull String[] keys) { - String[] arguments = ArrayUtils.addFirst(keys, destination); - return commandManager.submitNewCommand(SInterStore, arguments, this::handleLongResponse); + public CompletableFuture setrange(@NonNull String key, int offset, @NonNull String value) { + String[] arguments = new String[] {key, Integer.toString(offset), value}; + return commandManager.submitNewCommand(SetRange, arguments, this::handleLongResponse); } @Override - public CompletableFuture> sinter(@NonNull String[] keys) { - return commandManager.submitNewCommand(SInter, keys, this::handleSetResponse); + public CompletableFuture setrange( + @NonNull GlideString key, int offset, @NonNull GlideString value) { + GlideString[] arguments = new GlideString[] {key, gs(Integer.toString(offset)), value}; + return commandManager.submitNewCommand(SetRange, arguments, this::handleLongResponse); } @Override - public CompletableFuture sunionstore(@NonNull String destination, @NonNull String[] keys) { - String[] arguments = ArrayUtils.addFirst(keys, destination); - return commandManager.submitNewCommand(SUnionStore, arguments, this::handleLongResponse); + public CompletableFuture getrange(@NonNull String key, int start, int end) { + String[] arguments = new String[] {key, Integer.toString(start), Integer.toString(end)}; + return commandManager.submitNewCommand(GetRange, arguments, this::handleStringResponse); } @Override - public CompletableFuture exists(@NonNull String[] keys) { - return commandManager.submitNewCommand(Exists, keys, this::handleLongResponse); + public CompletableFuture getrange(@NonNull GlideString key, int start, int end) { + GlideString[] arguments = + new GlideString[] {key, gs(Integer.toString(start)), gs(Integer.toString(end))}; + return commandManager.submitNewCommand(GetRange, arguments, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture hget(@NonNull String key, @NonNull String field) { + return commandManager.submitNewCommand( + HGet, new String[] {key, field}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture hget(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HGet, new GlideString[] {key, field}, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture hset( + @NonNull String key, @NonNull Map fieldValueMap) { + String[] args = ArrayUtils.addFirst(convertMapToKeyValueStringArray(fieldValueMap), key); + return commandManager.submitNewCommand(HSet, args, this::handleLongResponse); + } + + @Override + public CompletableFuture hset( + @NonNull GlideString key, @NonNull Map fieldValueMap) { + GlideString[] args = + ArrayUtils.addFirst(convertMapToKeyValueGlideStringArray(fieldValueMap), key); + return commandManager.submitNewCommand(HSet, args, this::handleLongResponse); + } + + @Override + public CompletableFuture hsetnx( + @NonNull String key, @NonNull String field, @NonNull String value) { + return commandManager.submitNewCommand( + HSetNX, new String[] {key, field, value}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture hsetnx( + @NonNull GlideString key, @NonNull GlideString field, @NonNull GlideString value) { + return commandManager.submitNewCommand( + HSetNX, new GlideString[] {key, field, value}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture hdel(@NonNull String key, @NonNull String[] fields) { + String[] args = ArrayUtils.addFirst(fields, key); + return commandManager.submitNewCommand(HDel, args, this::handleLongResponse); + } + + @Override + public CompletableFuture hdel(@NonNull GlideString key, @NonNull GlideString[] fields) { + GlideString[] args = ArrayUtils.addFirst(fields, key); + return commandManager.submitNewCommand(HDel, args, this::handleLongResponse); + } + + @Override + public CompletableFuture hlen(@NonNull String key) { + return commandManager.submitNewCommand(HLen, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture hlen(@NonNull GlideString key) { + return commandManager.submitNewCommand(HLen, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture hvals(@NonNull String key) { + return commandManager.submitNewCommand( + HVals, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture hvals(@NonNull GlideString key) { + return commandManager.submitNewCommand( + HVals, + new GlideString[] {key}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture hmget(@NonNull String key, @NonNull String[] fields) { + String[] arguments = ArrayUtils.addFirst(fields, key); + return commandManager.submitNewCommand( + HMGet, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture hmget( + @NonNull GlideString key, @NonNull GlideString[] fields) { + GlideString[] arguments = ArrayUtils.addFirst(fields, key); + return commandManager.submitNewCommand( + HMGet, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture hexists(@NonNull String key, @NonNull String field) { + return commandManager.submitNewCommand( + HExists, new String[] {key, field}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture hexists(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HExists, new GlideString[] {key, field}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture> hgetall(@NonNull String key) { + return commandManager.submitNewCommand(HGetAll, new String[] {key}, this::handleMapResponse); + } + + @Override + public CompletableFuture> hgetall(@NonNull GlideString key) { + return commandManager.submitNewCommand( + HGetAll, new GlideString[] {key}, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture hincrBy(@NonNull String key, @NonNull String field, long amount) { + return commandManager.submitNewCommand( + HIncrBy, new String[] {key, field, Long.toString(amount)}, this::handleLongResponse); + } + + @Override + public CompletableFuture hincrBy( + @NonNull GlideString key, @NonNull GlideString field, long amount) { + return commandManager.submitNewCommand( + HIncrBy, + new GlideString[] {key, field, gs(Long.toString(amount))}, + this::handleLongResponse); + } + + @Override + public CompletableFuture hincrByFloat( + @NonNull String key, @NonNull String field, double amount) { + return commandManager.submitNewCommand( + HIncrByFloat, + new String[] {key, field, Double.toString(amount)}, + this::handleDoubleResponse); + } + + @Override + public CompletableFuture hincrByFloat( + @NonNull GlideString key, @NonNull GlideString field, double amount) { + return commandManager.submitNewCommand( + HIncrByFloat, + new GlideString[] {key, field, gs(Double.toString(amount))}, + this::handleDoubleResponse); + } + + @Override + public CompletableFuture hkeys(@NonNull String key) { + return commandManager.submitNewCommand( + HKeys, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture hkeys(@NonNull GlideString key) { + return commandManager.submitNewCommand( + HKeys, + new GlideString[] {key}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture hstrlen(@NonNull String key, @NonNull String field) { + return commandManager.submitNewCommand( + HStrlen, new String[] {key, field}, this::handleLongResponse); + } + + @Override + public CompletableFuture hstrlen(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HStrlen, new GlideString[] {key, field}, this::handleLongResponse); + } + + @Override + public CompletableFuture hrandfield(@NonNull String key) { + return commandManager.submitNewCommand( + HRandField, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture hrandfield(@NonNull GlideString key) { + return commandManager.submitNewCommand( + HRandField, new GlideString[] {key}, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture hrandfieldWithCount(@NonNull String key, long count) { + return commandManager.submitNewCommand( + HRandField, + new String[] {key, Long.toString(count)}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture hrandfieldWithCount( + @NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + HRandField, + new GlideString[] {key, GlideString.of(count)}, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture hrandfieldWithCountWithValues( + @NonNull String key, long count) { + return commandManager.submitNewCommand( + HRandField, + new String[] {key, Long.toString(count), WITH_VALUES_VALKEY_API}, + response -> castArrayofArrays(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture hrandfieldWithCountWithValues( + @NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + HRandField, + new GlideString[] {key, GlideString.of(count), GlideString.of(WITH_VALUES_VALKEY_API)}, + response -> castArrayofArrays(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture lpush(@NonNull String key, @NonNull String[] elements) { + String[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture lpush(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture lpop(@NonNull String key) { + return commandManager.submitNewCommand( + LPop, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture lpop(@NonNull GlideString key) { + return commandManager.submitNewCommand( + LPop, new GlideString[] {key}, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture lpopCount(@NonNull String key, long count) { + return commandManager.submitNewCommand( + LPop, + new String[] {key, Long.toString(count)}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture lpopCount(@NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + LPop, + new GlideString[] {key, gs(Long.toString(count))}, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture lpos(@NonNull String key, @NonNull String element) { + return commandManager.submitNewCommand( + LPos, new String[] {key, element}, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture lpos(@NonNull GlideString key, @NonNull GlideString element) { + return commandManager.submitNewCommand( + LPos, new GlideString[] {key, element}, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture lpos( + @NonNull String key, @NonNull String element, @NonNull LPosOptions options) { + String[] arguments = concatenateArrays(new String[] {key, element}, options.toArgs()); + return commandManager.submitNewCommand(LPos, arguments, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture lpos( + @NonNull GlideString key, @NonNull GlideString element, @NonNull LPosOptions options) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(element).add(options.toArgs()).toArray(); + return commandManager.submitNewCommand(LPos, arguments, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture lposCount( + @NonNull String key, @NonNull String element, long count) { + return commandManager.submitNewCommand( + LPos, + new String[] {key, element, COUNT_VALKEY_API, Long.toString(count)}, + response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture lposCount( + @NonNull GlideString key, @NonNull GlideString element, long count) { + return commandManager.submitNewCommand( + LPos, + new GlideString[] {key, element, gs(COUNT_VALKEY_API), gs(Long.toString(count))}, + response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture lposCount( + @NonNull String key, @NonNull String element, long count, @NonNull LPosOptions options) { + String[] arguments = + concatenateArrays( + new String[] {key, element, COUNT_VALKEY_API, Long.toString(count)}, options.toArgs()); + + return commandManager.submitNewCommand( + LPos, arguments, response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture lposCount( + @NonNull GlideString key, + @NonNull GlideString element, + long count, + @NonNull LPosOptions options) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(element) + .add(COUNT_VALKEY_API) + .add(count) + .add(options.toArgs()) + .toArray(); + + return commandManager.submitNewCommand( + LPos, arguments, response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture lrange(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + LRange, + new String[] {key, Long.toString(start), Long.toString(end)}, + response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture lrange(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + LRange, + new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture lindex(@NonNull String key, long index) { + return commandManager.submitNewCommand( + LIndex, new String[] {key, Long.toString(index)}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture lindex(@NonNull GlideString key, long index) { + return commandManager.submitNewCommand( + LIndex, + new GlideString[] {key, gs(Long.toString(index))}, + this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture ltrim(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + LTrim, + new String[] {key, Long.toString(start), Long.toString(end)}, + this::handleStringResponse); + } + + @Override + public CompletableFuture ltrim(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + LTrim, + new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}, + this::handleStringResponse); + } + + @Override + public CompletableFuture llen(@NonNull String key) { + return commandManager.submitNewCommand(LLen, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture llen(@NonNull GlideString key) { + return commandManager.submitNewCommand(LLen, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture lrem(@NonNull String key, long count, @NonNull String element) { + return commandManager.submitNewCommand( + LRem, new String[] {key, Long.toString(count), element}, this::handleLongResponse); + } + + @Override + public CompletableFuture lrem( + @NonNull GlideString key, long count, @NonNull GlideString element) { + return commandManager.submitNewCommand( + LRem, new GlideString[] {key, gs(Long.toString(count)), element}, this::handleLongResponse); + } + + @Override + public CompletableFuture rpush(@NonNull String key, @NonNull String[] elements) { + String[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture rpush(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture rpop(@NonNull String key) { + return commandManager.submitNewCommand( + RPop, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture rpop(@NonNull GlideString key) { + return commandManager.submitNewCommand( + RPop, new GlideString[] {key}, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture rpopCount(@NonNull String key, long count) { + return commandManager.submitNewCommand( + RPop, + new String[] {key, Long.toString(count)}, + response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture rpopCount(@NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + RPop, + new GlideString[] {key, gs(Long.toString(count))}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture sadd(@NonNull String key, @NonNull String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sadd(@NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sismember(@NonNull String key, @NonNull String member) { + return commandManager.submitNewCommand( + SIsMember, new String[] {key, member}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture sismember( + @NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + SIsMember, new GlideString[] {key, member}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture srem(@NonNull String key, @NonNull String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SRem, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture srem(@NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(SRem, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture> smembers(@NonNull String key) { + return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); + } + + @Override + public CompletableFuture> smembers(@NonNull GlideString key) { + return commandManager.submitNewCommand( + SMembers, new GlideString[] {key}, this::handleSetBinaryResponse); + } + + @Override + public CompletableFuture scard(@NonNull String key) { + return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture scard(@NonNull GlideString key) { + return commandManager.submitNewCommand( + SCard, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture> sdiff(@NonNull String[] keys) { + return commandManager.submitNewCommand(SDiff, keys, this::handleSetResponse); + } + + @Override + public CompletableFuture> sdiff(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(SDiff, keys, this::handleSetBinaryResponse); + } + + @Override + public CompletableFuture smismember(@NonNull String key, @NonNull String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand( + SMIsMember, arguments, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture smismember( + @NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand( + SMIsMember, arguments, response -> castArray(handleArrayResponse(response), Boolean.class)); + } + + @Override + public CompletableFuture sdiffstore(@NonNull String destination, @NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SDiffStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sdiffstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SDiffStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture smove( + @NonNull String source, @NonNull String destination, @NonNull String member) { + return commandManager.submitNewCommand( + SMove, new String[] {source, destination, member}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture smove( + @NonNull GlideString source, @NonNull GlideString destination, @NonNull GlideString member) { + return commandManager.submitNewCommand( + SMove, new GlideString[] {source, destination, member}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture sinterstore(@NonNull String destination, @NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SInterStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sinterstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SInterStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture> sinter(@NonNull String[] keys) { + return commandManager.submitNewCommand(SInter, keys, this::handleSetResponse); + } + + @Override + public CompletableFuture> sinter(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(SInter, keys, this::handleSetBinaryResponse); + } + + @Override + public CompletableFuture sunionstore(@NonNull String destination, @NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SUnionStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sunionstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = ArrayUtils.addFirst(keys, destination); + return commandManager.submitNewCommand(SUnionStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture exists(@NonNull String[] keys) { + return commandManager.submitNewCommand(Exists, keys, this::handleLongResponse); + } + + @Override + public CompletableFuture exists(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(Exists, keys, this::handleLongResponse); } @Override @@ -600,389 +1633,3298 @@ public CompletableFuture unlink(@NonNull String[] keys) { } @Override - public CompletableFuture expire(@NonNull String key, long seconds) { + public CompletableFuture unlink(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(Unlink, keys, this::handleLongResponse); + } + + @Override + public CompletableFuture expire(@NonNull String key, long seconds) { + return commandManager.submitNewCommand( + Expire, new String[] {key, Long.toString(seconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expire(@NonNull GlideString key, long seconds) { + return commandManager.submitNewCommand( + Expire, new GlideString[] {key, gs(Long.toString(seconds))}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expire( + @NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(Expire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expire( + @NonNull GlideString key, long seconds, @NonNull ExpireOptions expireOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(seconds).add(expireOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand(Expire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt(@NonNull String key, long unixSeconds) { + return commandManager.submitNewCommand( + ExpireAt, new String[] {key, Long.toString(unixSeconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt(@NonNull GlideString key, long unixSeconds) { + return commandManager.submitNewCommand( + ExpireAt, + new GlideString[] {key, gs(Long.toString(unixSeconds))}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt( + @NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(ExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt( + @NonNull GlideString key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(unixSeconds).add(expireOptions.toArgs()).toArray(); + return commandManager.submitNewCommand(ExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire(@NonNull String key, long milliseconds) { + return commandManager.submitNewCommand( + PExpire, new String[] {key, Long.toString(milliseconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire(@NonNull GlideString key, long milliseconds) { + return commandManager.submitNewCommand( + PExpire, + new GlideString[] {key, gs(Long.toString(milliseconds))}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire( + @NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(PExpire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire( + @NonNull GlideString key, long milliseconds, @NonNull ExpireOptions expireOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(milliseconds).add(expireOptions.toArgs()).toArray(); + return commandManager.submitNewCommand(PExpire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt(@NonNull String key, long unixMilliseconds) { + return commandManager.submitNewCommand( + PExpireAt, + new String[] {key, Long.toString(unixMilliseconds)}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt(@NonNull GlideString key, long unixMilliseconds) { + return commandManager.submitNewCommand( + PExpireAt, + new GlideString[] {key, gs(Long.toString(unixMilliseconds))}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt( + @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll( + new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(PExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt( + @NonNull GlideString key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(unixMilliseconds).add(expireOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand(PExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture ttl(@NonNull String key) { + return commandManager.submitNewCommand(TTL, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture ttl(@NonNull GlideString key) { + return commandManager.submitNewCommand(TTL, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture expiretime(@NonNull String key) { + return commandManager.submitNewCommand( + ExpireTime, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture expiretime(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ExpireTime, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture pexpiretime(@NonNull String key) { + return commandManager.submitNewCommand( + PExpireTime, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture pexpiretime(@NonNull GlideString key) { + return commandManager.submitNewCommand( + PExpireTime, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture invokeScript(@NonNull Script script) { + if (script.getBinaryOutput()) { + return commandManager.submitScript( + script, List.of(), List.of(), this::handleBinaryObjectOrNullResponse); + } else { + return commandManager.submitScript( + script, List.of(), List.of(), this::handleObjectOrNullResponse); + } + } + + @Override + public CompletableFuture invokeScript( + @NonNull Script script, @NonNull ScriptOptions options) { + if (script.getBinaryOutput()) { + return commandManager.submitScript( + script, + options.getKeys().stream().map(GlideString::gs).collect(Collectors.toList()), + options.getArgs().stream().map(GlideString::gs).collect(Collectors.toList()), + this::handleBinaryObjectOrNullResponse); + } else { + return commandManager.submitScript( + script, + options.getKeys().stream().map(GlideString::gs).collect(Collectors.toList()), + options.getArgs().stream().map(GlideString::gs).collect(Collectors.toList()), + this::handleObjectOrNullResponse); + } + } + + @Override + public CompletableFuture invokeScript( + @NonNull Script script, @NonNull ScriptOptionsGlideString options) { + if (script.getBinaryOutput()) { + return commandManager.submitScript( + script, options.getKeys(), options.getArgs(), this::handleBinaryObjectOrNullResponse); + } else { + return commandManager.submitScript( + script, options.getKeys(), options.getArgs(), this::handleObjectOrNullResponse); + } + } + + @Override + public CompletableFuture zadd( + @NonNull String key, + @NonNull Map membersScoresMap, + @NonNull ZAddOptions options, + boolean changed) { + String[] changedArg = changed ? new String[] {"CH"} : new String[] {}; + String[] membersScores = convertMapToValueKeyStringArray(membersScoresMap); + + String[] arguments = + concatenateArrays(new String[] {key}, options.toArgs(), changedArg, membersScores); + + return commandManager.submitNewCommand(ZAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zadd( + @NonNull GlideString key, + @NonNull Map membersScoresMap, + @NonNull ZAddOptions options, + boolean changed) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(options.toArgs()) + .addIf("CH", changed) + .add(convertMapToValueKeyStringArrayBinary(membersScoresMap)) + .toArray(); + + return commandManager.submitNewCommand(ZAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zadd( + @NonNull String key, + @NonNull Map membersScoresMap, + @NonNull ZAddOptions options) { + return this.zadd(key, membersScoresMap, options, false); + } + + @Override + public CompletableFuture zadd( + @NonNull GlideString key, + @NonNull Map membersScoresMap, + @NonNull ZAddOptions options) { + return this.zadd(key, membersScoresMap, options, false); + } + + @Override + public CompletableFuture zadd( + @NonNull String key, @NonNull Map membersScoresMap, boolean changed) { + return this.zadd(key, membersScoresMap, ZAddOptions.builder().build(), changed); + } + + @Override + public CompletableFuture zadd( + @NonNull GlideString key, + @NonNull Map membersScoresMap, + boolean changed) { + return this.zadd(key, membersScoresMap, ZAddOptions.builder().build(), changed); + } + + @Override + public CompletableFuture zadd( + @NonNull String key, @NonNull Map membersScoresMap) { + return this.zadd(key, membersScoresMap, ZAddOptions.builder().build(), false); + } + + @Override + public CompletableFuture zadd( + @NonNull GlideString key, @NonNull Map membersScoresMap) { + return this.zadd(key, membersScoresMap, ZAddOptions.builder().build(), false); + } + + @Override + public CompletableFuture zaddIncr( + @NonNull String key, @NonNull String member, double increment, @NonNull ZAddOptions options) { + String[] arguments = + concatenateArrays( + new String[] {key}, + options.toArgs(), + new String[] {"INCR", Double.toString(increment), member}); + + return commandManager.submitNewCommand(ZAdd, arguments, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture zaddIncr( + @NonNull GlideString key, + @NonNull GlideString member, + double increment, + @NonNull ZAddOptions options) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {key}, + options.toArgsBinary(), + new GlideString[] {gs("INCR"), gs(Double.toString(increment)), member}); + + return commandManager.submitNewCommand(ZAdd, arguments, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture zaddIncr( + @NonNull String key, @NonNull String member, double increment) { + String[] arguments = + concatenateArrays( + new String[] {key}, new String[] {"INCR", Double.toString(increment), member}); + + return commandManager.submitNewCommand(ZAdd, arguments, this::handleDoubleResponse); + } + + @Override + public CompletableFuture zaddIncr( + @NonNull GlideString key, @NonNull GlideString member, double increment) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {key}, + new GlideString[] {gs("INCR"), gs(Double.toString(increment)), member}); + + return commandManager.submitNewCommand(ZAdd, arguments, this::handleDoubleResponse); + } + + @Override + public CompletableFuture zrem(@NonNull String key, @NonNull String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(ZRem, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zrem(@NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(ZRem, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zcard(@NonNull String key) { + return commandManager.submitNewCommand(ZCard, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture zcard(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ZCard, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture> zpopmin(@NonNull String key, long count) { + return commandManager.submitNewCommand( + ZPopMin, new String[] {key, Long.toString(count)}, this::handleMapResponse); + } + + @Override + public CompletableFuture> zpopmin(@NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + ZPopMin, + new GlideString[] {key, gs(Long.toString(count))}, + this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> zpopmin(@NonNull String key) { + return commandManager.submitNewCommand(ZPopMin, new String[] {key}, this::handleMapResponse); + } + + @Override + public CompletableFuture> zpopmin(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ZPopMin, new GlideString[] {key}, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture bzpopmin(@NonNull String[] keys, double timeout) { + String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); + return commandManager.submitNewCommand(BZPopMin, arguments, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture bzpopmin(@NonNull GlideString[] keys, double timeout) { + GlideString[] arguments = ArrayUtils.add(keys, gs(Double.toString(timeout))); + return commandManager.submitNewCommand( + BZPopMin, arguments, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture> zpopmax(@NonNull String key, long count) { + return commandManager.submitNewCommand( + ZPopMax, new String[] {key, Long.toString(count)}, this::handleMapResponse); + } + + @Override + public CompletableFuture> zpopmax(@NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + ZPopMax, + new GlideString[] {key, gs(Long.toString(count))}, + this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> zpopmax(@NonNull String key) { + return commandManager.submitNewCommand(ZPopMax, new String[] {key}, this::handleMapResponse); + } + + @Override + public CompletableFuture> zpopmax(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ZPopMax, new GlideString[] {key}, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture bzpopmax(@NonNull String[] keys, double timeout) { + String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); + return commandManager.submitNewCommand(BZPopMax, arguments, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture bzpopmax(@NonNull GlideString[] keys, double timeout) { + GlideString[] arguments = ArrayUtils.add(keys, gs(Double.toString(timeout))); + return commandManager.submitNewCommand( + BZPopMax, arguments, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture zscore(@NonNull String key, @NonNull String member) { + return commandManager.submitNewCommand( + ZScore, new String[] {key, member}, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture zscore(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZScore, new GlideString[] {key, member}, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture zrank(@NonNull String key, @NonNull String member) { + return commandManager.submitNewCommand( + ZRank, new String[] {key, member}, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture zrank(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZRank, new GlideString[] {key, member}, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture zrankWithScore(@NonNull String key, @NonNull String member) { + return commandManager.submitNewCommand( + ZRank, new String[] {key, member, WITH_SCORE_VALKEY_API}, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture zrevrank(@NonNull String key, @NonNull String member) { + return commandManager.submitNewCommand( + ZRevRank, new String[] {key, member}, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture zrevrank(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZRevRank, new GlideString[] {key, member}, this::handleLongOrNullResponse); + } + + @Override + public CompletableFuture zrevrankWithScore( + @NonNull String key, @NonNull String member) { + return commandManager.submitNewCommand( + ZRevRank, + new String[] {key, member, WITH_SCORE_VALKEY_API}, + this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture zrevrankWithScore( + @NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZRevRank, + new GlideString[] {key, member, gs(WITH_SCORE_VALKEY_API)}, + this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture zmscore(@NonNull String key, @NonNull String[] members) { + String[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand( + ZMScore, + arguments, + response -> castArray(handleArrayOrNullResponse(response), Double.class)); + } + + @Override + public CompletableFuture zmscore( + @NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand( + ZMScore, + arguments, + response -> castArray(handleArrayOrNullResponse(response), Double.class)); + } + + @Override + public CompletableFuture zdiff(@NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, Long.toString(keys.length)); + return commandManager.submitNewCommand( + ZDiff, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture zdiff(@NonNull GlideString[] keys) { + GlideString[] arguments = new ArgsBuilder().add(keys.length).add(keys).toArray(); + return commandManager.submitNewCommand( + ZDiff, + arguments, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture> zdiffWithScores(@NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, Long.toString(keys.length)); + arguments = ArrayUtils.add(arguments, WITH_SCORES_VALKEY_API); + return commandManager.submitNewCommand(ZDiff, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> zdiffWithScores(@NonNull GlideString[] keys) { + GlideString[] arguments = new ArgsBuilder().add(keys.length).add(keys).toArray(); + arguments = ArrayUtils.add(arguments, gs(WITH_SCORES_VALKEY_API)); + return commandManager.submitNewCommand(ZDiff, arguments, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture zdiffstore(@NonNull String destination, @NonNull String[] keys) { + String[] arguments = + ArrayUtils.addAll(new String[] {destination, Long.toString(keys.length)}, keys); + return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zdiffstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = + ArrayUtils.addAll(new GlideString[] {destination, gs(Long.toString(keys.length))}, keys); + return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zcount( + @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + return commandManager.submitNewCommand( + ZCount, new String[] {key, minScore.toArgs(), maxScore.toArgs()}, this::handleLongResponse); + } + + @Override + public CompletableFuture zcount( + @NonNull GlideString key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + return commandManager.submitNewCommand( + ZCount, + new ArgsBuilder().add(key).add(minScore.toArgs()).add(maxScore.toArgs()).toArray(), + this::handleLongResponse); + } + + @Override + public CompletableFuture zremrangebyrank(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + ZRemRangeByRank, + new String[] {key, Long.toString(start), Long.toString(end)}, + this::handleLongResponse); + } + + @Override + public CompletableFuture zremrangebyrank(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + ZRemRangeByRank, + new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}, + this::handleLongResponse); + } + + @Override + public CompletableFuture zremrangebylex( + @NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + return commandManager.submitNewCommand( + ZRemRangeByLex, + new String[] {key, minLex.toArgs(), maxLex.toArgs()}, + this::handleLongResponse); + } + + @Override + public CompletableFuture zremrangebylex( + @NonNull GlideString key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + return commandManager.submitNewCommand( + ZRemRangeByLex, + new ArgsBuilder().add(key).add(minLex.toArgs()).add(maxLex.toArgs()).toArray(), + this::handleLongResponse); + } + + @Override + public CompletableFuture zremrangebyscore( + @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + return commandManager.submitNewCommand( + ZRemRangeByScore, + new String[] {key, minScore.toArgs(), maxScore.toArgs()}, + this::handleLongResponse); + } + + @Override + public CompletableFuture zremrangebyscore( + @NonNull GlideString key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + return commandManager.submitNewCommand( + ZRemRangeByScore, + new ArgsBuilder().add(key).add(minScore.toArgs()).add(maxScore.toArgs()).toArray(), + this::handleLongResponse); + } + + @Override + public CompletableFuture zlexcount( + @NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + return commandManager.submitNewCommand( + ZLexCount, new String[] {key, minLex.toArgs(), maxLex.toArgs()}, this::handleLongResponse); + } + + @Override + public CompletableFuture zlexcount( + @NonNull GlideString key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + return commandManager.submitNewCommand( + ZLexCount, + new ArgsBuilder().add(key).add(minLex.toArgs()).add(maxLex.toArgs()).toArray(), + this::handleLongResponse); + } + + @Override + public CompletableFuture zrangestore( + @NonNull String destination, + @NonNull String source, + @NonNull RangeQuery rangeQuery, + boolean reverse) { + String[] arguments = + RangeOptions.createZRangeStoreArgs(destination, source, rangeQuery, reverse); + + return commandManager.submitNewCommand(ZRangeStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zrangestore( + @NonNull GlideString destination, + @NonNull GlideString source, + @NonNull RangeQuery rangeQuery, + boolean reverse) { + GlideString[] arguments = + RangeOptions.createZRangeStoreArgsBinary(destination, source, rangeQuery, reverse); + + return commandManager.submitNewCommand(ZRangeStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zrangestore( + @NonNull String destination, @NonNull String source, @NonNull RangeQuery rangeQuery) { + return zrangestore(destination, source, rangeQuery, false); + } + + @Override + public CompletableFuture zrangestore( + @NonNull GlideString destination, + @NonNull GlideString source, + @NonNull RangeQuery rangeQuery) { + return zrangestore(destination, source, rangeQuery, false); + } + + @Override + public CompletableFuture zunionstore( + @NonNull String destination, + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + String[] arguments = + concatenateArrays( + new String[] {destination}, keysOrWeightedKeys.toArgs(), aggregate.toArgs()); + return commandManager.submitNewCommand(ZUnionStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zunionstore( + @NonNull GlideString destination, + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .toArray(); + + return commandManager.submitNewCommand(ZUnionStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zunionstore( + @NonNull String destination, @NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + String[] arguments = concatenateArrays(new String[] {destination}, keysOrWeightedKeys.toArgs()); + return commandManager.submitNewCommand(ZUnionStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zunionstore( + @NonNull GlideString destination, @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + GlideString[] arguments = + new ArgsBuilder().add(destination).add(keysOrWeightedKeys.toArgs()).toArray(); + return commandManager.submitNewCommand(ZUnionStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zinterstore( + @NonNull String destination, + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + String[] arguments = + concatenateArrays( + new String[] {destination}, keysOrWeightedKeys.toArgs(), aggregate.toArgs()); + return commandManager.submitNewCommand(ZInterStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zinterstore( + @NonNull GlideString destination, + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .toArray(); + return commandManager.submitNewCommand(ZInterStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zinterstore( + @NonNull String destination, @NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + String[] arguments = concatenateArrays(new String[] {destination}, keysOrWeightedKeys.toArgs()); + return commandManager.submitNewCommand(ZInterStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zinterstore( + @NonNull GlideString destination, @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + GlideString[] arguments = + concatenateArrays(new GlideString[] {destination}, keysOrWeightedKeys.toArgs()); + return commandManager.submitNewCommand(ZInterStore, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zunion(@NonNull KeyArray keys) { + return commandManager.submitNewCommand( + ZUnion, keys.toArgs(), response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture zunion(@NonNull KeyArrayBinary keys) { + return commandManager.submitNewCommand( + ZUnion, + keys.toArgs(), + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture> zunionWithScores( + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, @NonNull Aggregate aggregate) { + String[] arguments = + concatenateArrays( + keysOrWeightedKeys.toArgs(), aggregate.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + return commandManager.submitNewCommand(ZUnion, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> zunionWithScores( + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, @NonNull Aggregate aggregate) { + GlideString[] arguments = + new ArgsBuilder() + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API) + .toArray(); + return commandManager.submitNewCommand(ZUnion, arguments, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> zunionWithScores( + @NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + String[] arguments = + concatenateArrays(keysOrWeightedKeys.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + return commandManager.submitNewCommand(ZUnion, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> zunionWithScores( + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + GlideString[] arguments = + new ArgsBuilder().add(keysOrWeightedKeys.toArgs()).add(WITH_SCORES_VALKEY_API).toArray(); + + return commandManager.submitNewCommand(ZUnion, arguments, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture zinter(@NonNull KeyArray keys) { + return commandManager.submitNewCommand( + ZInter, keys.toArgs(), response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture zinter(@NonNull KeyArrayBinary keys) { + return commandManager.submitNewCommand( + ZInter, + keys.toArgs(), + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture> zinterWithScores( + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, @NonNull Aggregate aggregate) { + String[] arguments = + concatenateArrays( + keysOrWeightedKeys.toArgs(), aggregate.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + return commandManager.submitNewCommand(ZInter, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> zinterWithScores( + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, @NonNull Aggregate aggregate) { + GlideString[] arguments = + new ArgsBuilder() + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API) + .toArray(); + return commandManager.submitNewCommand(ZInter, arguments, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> zinterWithScores( + @NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + String[] arguments = + concatenateArrays(keysOrWeightedKeys.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + return commandManager.submitNewCommand(ZInter, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> zinterWithScores( + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + GlideString[] arguments = + concatenateArrays( + keysOrWeightedKeys.toArgs(), new GlideString[] {gs(WITH_SCORES_VALKEY_API)}); + return commandManager.submitNewCommand(ZInter, arguments, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture zrandmember(@NonNull String key) { + return commandManager.submitNewCommand( + ZRandMember, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture zrandmember(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ZRandMember, new GlideString[] {key}, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture zrandmemberWithCount(@NonNull String key, long count) { + return commandManager.submitNewCommand( + ZRandMember, + new String[] {key, Long.toString(count)}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture zrandmemberWithCount( + @NonNull GlideString key, long count) { + return commandManager.submitNewCommand( + ZRandMember, + new GlideString[] {key, gs(Long.toString(count))}, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture zrandmemberWithCountWithScores( + @NonNull String key, long count) { + String[] arguments = new String[] {key, Long.toString(count), WITH_SCORES_VALKEY_API}; + return commandManager.submitNewCommand( + ZRandMember, + arguments, + response -> castArray(handleArrayResponse(response), Object[].class)); + } + + @Override + public CompletableFuture zrandmemberWithCountWithScores( + @NonNull GlideString key, long count) { + GlideString[] arguments = + new GlideString[] {key, gs(Long.toString(count)), gs(WITH_SCORES_VALKEY_API)}; + return commandManager.submitNewCommand( + ZRandMember, + arguments, + response -> castArray(handleArrayResponseBinary(response), Object[].class)); + } + + @Override + public CompletableFuture zincrby( + @NonNull String key, double increment, @NonNull String member) { + String[] arguments = new String[] {key, Double.toString(increment), member}; + return commandManager.submitNewCommand(ZIncrBy, arguments, this::handleDoubleResponse); + } + + @Override + public CompletableFuture zincrby( + @NonNull GlideString key, double increment, @NonNull GlideString member) { + GlideString[] arguments = new GlideString[] {key, gs(Double.toString(increment)), member}; + return commandManager.submitNewCommand(ZIncrBy, arguments, this::handleDoubleResponse); + } + + @Override + public CompletableFuture zintercard(@NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, Integer.toString(keys.length)); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zintercard(@NonNull GlideString[] keys) { + GlideString[] arguments = ArrayUtils.addFirst(keys, gs(Integer.toString(keys.length))); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zintercard(@NonNull String[] keys, long limit) { + String[] arguments = + concatenateArrays( + new String[] {Integer.toString(keys.length)}, + keys, + new String[] {LIMIT_VALKEY_API, Long.toString(limit)}); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zintercard(@NonNull GlideString[] keys, long limit) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Integer.toString(keys.length))}, + keys, + new GlideString[] {gs(LIMIT_VALKEY_API), gs(Long.toString(limit))}); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture xadd(@NonNull String key, @NonNull Map values) { + return xadd(key, values, StreamAddOptions.builder().build()); + } + + @Override + public CompletableFuture xadd( + @NonNull GlideString key, @NonNull Map values) { + return xadd(key, values, StreamAddOptionsBinary.builder().build()); + } + + @Override + public CompletableFuture xadd( + @NonNull String key, @NonNull Map values, @NonNull StreamAddOptions options) { + String[] arguments = + ArrayUtils.addAll( + ArrayUtils.addFirst(options.toArgs(), key), convertMapToKeyValueStringArray(values)); + return commandManager.submitNewCommand(XAdd, arguments, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture xadd( + @NonNull GlideString key, + @NonNull Map values, + @NonNull StreamAddOptionsBinary options) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(options.toArgs()) + .add(convertMapToKeyValueGlideStringArray(values)) + .toArray(); + + return commandManager.submitNewCommand(XAdd, arguments, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture>> xread( + @NonNull Map keysAndIds) { + return xread(keysAndIds, StreamReadOptions.builder().build()); + } + + @Override + public CompletableFuture>> xreadBinary( + @NonNull Map keysAndIds) { + return xreadBinary(keysAndIds, StreamReadOptions.builder().build()); + } + + @Override + public CompletableFuture>> xread( + @NonNull Map keysAndIds, @NonNull StreamReadOptions options) { + String[] arguments = options.toArgs(keysAndIds); + return commandManager.submitNewCommand(XRead, arguments, this::handleXReadResponse); + } + + @Override + public CompletableFuture>> xreadBinary( + @NonNull Map keysAndIds, @NonNull StreamReadOptions options) { + GlideString[] arguments = options.toArgsBinary(keysAndIds); + return commandManager.submitNewCommand(XRead, arguments, this::handleXReadResponseBinary); + } + + @Override + public CompletableFuture xtrim(@NonNull String key, @NonNull StreamTrimOptions options) { + String[] arguments = ArrayUtils.addFirst(options.toArgs(), key); + return commandManager.submitNewCommand(XTrim, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture xtrim( + @NonNull GlideString key, @NonNull StreamTrimOptions options) { + GlideString[] arguments = new ArgsBuilder().add(key).add(options.toArgs()).toArray(); + + return commandManager.submitNewCommand(XTrim, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture xlen(@NonNull String key) { + return commandManager.submitNewCommand(XLen, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture xlen(@NonNull GlideString key) { + return commandManager.submitNewCommand(XLen, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture xdel(@NonNull String key, @NonNull String[] ids) { + String[] arguments = ArrayUtils.addFirst(ids, key); + return commandManager.submitNewCommand(XDel, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture xdel(@NonNull GlideString key, @NonNull GlideString[] ids) { + GlideString[] arguments = ArrayUtils.addFirst(ids, key); + return commandManager.submitNewCommand(XDel, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture> xrange( + @NonNull String key, @NonNull StreamRange start, @NonNull StreamRange end) { + String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(start, end), key); + return commandManager.submitNewCommand( + XRange, arguments, response -> castMapOf2DArray(handleMapResponse(response), String.class)); + } + + @Override + public CompletableFuture> xrange( + @NonNull GlideString key, @NonNull StreamRange start, @NonNull StreamRange end) { + String[] toArgsString = StreamRange.toArgs(start, end); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> xrange( + @NonNull String key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { + String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(start, end, count), key); + return commandManager.submitNewCommand( + XRange, arguments, response -> castMapOf2DArray(handleMapResponse(response), String.class)); + } + + @Override + public CompletableFuture> xrange( + @NonNull GlideString key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { + String[] toArgsString = StreamRange.toArgs(start, end, count); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> xrevrange( + @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start) { + String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(end, start), key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOf2DArray(handleMapResponse(response), String.class)); + } + + @Override + public CompletableFuture> xrevrange( + @NonNull GlideString key, @NonNull StreamRange end, @NonNull StreamRange start) { + String[] toArgsString = StreamRange.toArgs(end, start); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> xrevrange( + @NonNull String key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { + String[] arguments = ArrayUtils.addFirst(StreamRange.toArgs(end, start, count), key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOf2DArray(handleMapResponse(response), String.class)); + } + + @Override + public CompletableFuture> xrevrange( + @NonNull GlideString key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { + String[] toArgsString = StreamRange.toArgs(end, start, count); + GlideString[] toArgsBinary = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] arguments = ArrayUtils.addFirst(toArgsBinary, key); + return commandManager.submitNewCommand( + XRevRange, + arguments, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture xgroupCreate( + @NonNull String key, @NonNull String groupName, @NonNull String id) { + return commandManager.submitNewCommand( + XGroupCreate, new String[] {key, groupName, id}, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupCreate( + @NonNull GlideString key, @NonNull GlideString groupName, @NonNull GlideString id) { + return commandManager.submitNewCommand( + XGroupCreate, new GlideString[] {key, groupName, id}, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupCreate( + @NonNull String key, + @NonNull String groupName, + @NonNull String id, + @NonNull StreamGroupOptions options) { + String[] arguments = concatenateArrays(new String[] {key, groupName, id}, options.toArgs()); + return commandManager.submitNewCommand(XGroupCreate, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupCreate( + @NonNull GlideString key, + @NonNull GlideString groupName, + @NonNull GlideString id, + @NonNull StreamGroupOptions options) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(groupName).add(id).add(options.toArgs()).toArray(); + return commandManager.submitNewCommand(XGroupCreate, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupDestroy(@NonNull String key, @NonNull String groupname) { + return commandManager.submitNewCommand( + XGroupDestroy, new String[] {key, groupname}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture xgroupDestroy( + @NonNull GlideString key, @NonNull GlideString groupname) { + return commandManager.submitNewCommand( + XGroupDestroy, new GlideString[] {key, groupname}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture xgroupCreateConsumer( + @NonNull String key, @NonNull String group, @NonNull String consumer) { + return commandManager.submitNewCommand( + XGroupCreateConsumer, new String[] {key, group, consumer}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture xgroupCreateConsumer( + @NonNull GlideString key, @NonNull GlideString group, @NonNull GlideString consumer) { + return commandManager.submitNewCommand( + XGroupCreateConsumer, + new GlideString[] {key, group, consumer}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture xgroupDelConsumer( + @NonNull String key, @NonNull String group, @NonNull String consumer) { + return commandManager.submitNewCommand( + XGroupDelConsumer, new String[] {key, group, consumer}, this::handleLongResponse); + } + + @Override + public CompletableFuture xgroupDelConsumer( + @NonNull GlideString key, @NonNull GlideString group, @NonNull GlideString consumer) { + return commandManager.submitNewCommand( + XGroupDelConsumer, new GlideString[] {key, group, consumer}, this::handleLongResponse); + } + + @Override + public CompletableFuture xgroupSetId( + @NonNull String key, @NonNull String groupName, @NonNull String id) { + return commandManager.submitNewCommand( + XGroupSetId, new String[] {key, groupName, id}, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupSetId( + @NonNull GlideString key, @NonNull GlideString groupName, @NonNull GlideString id) { + return commandManager.submitNewCommand( + XGroupSetId, new GlideString[] {key, groupName, id}, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupSetId( + @NonNull String key, @NonNull String groupName, @NonNull String id, long entriesRead) { + String[] arguments = + new String[] {key, groupName, id, ENTRIES_READ_VALKEY_API, Long.toString(entriesRead)}; + return commandManager.submitNewCommand(XGroupSetId, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture xgroupSetId( + @NonNull GlideString key, + @NonNull GlideString groupName, + @NonNull GlideString id, + long entriesRead) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(groupName) + .add(id) + .add(ENTRIES_READ_VALKEY_API) + .add(entriesRead) + .toArray(); + return commandManager.submitNewCommand(XGroupSetId, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture>> xreadgroup( + @NonNull Map keysAndIds, @NonNull String group, @NonNull String consumer) { + return xreadgroup(keysAndIds, group, consumer, StreamReadGroupOptions.builder().build()); + } + + @Override + public CompletableFuture>> xreadgroup( + @NonNull Map keysAndIds, + @NonNull GlideString group, + @NonNull GlideString consumer) { + return xreadgroup(keysAndIds, group, consumer, StreamReadGroupOptions.builder().build()); + } + + @Override + public CompletableFuture>> xreadgroup( + @NonNull Map keysAndIds, + @NonNull String group, + @NonNull String consumer, + @NonNull StreamReadGroupOptions options) { + String[] arguments = options.toArgs(group, consumer, keysAndIds); + return commandManager.submitNewCommand(XReadGroup, arguments, this::handleXReadResponse); + } + + @Override + public CompletableFuture>> xreadgroup( + @NonNull Map keysAndIds, + @NonNull GlideString group, + @NonNull GlideString consumer, + @NonNull StreamReadGroupOptions options) { + GlideString[] arguments = options.toArgsBinary(group, consumer, keysAndIds); + return commandManager.submitNewCommand(XReadGroup, arguments, this::handleXReadResponseBinary); + } + + @Override + public CompletableFuture xack( + @NonNull String key, @NonNull String group, @NonNull String[] ids) { + String[] args = concatenateArrays(new String[] {key, group}, ids); + return commandManager.submitNewCommand(XAck, args, this::handleLongResponse); + } + + @Override + public CompletableFuture xack( + @NonNull GlideString key, @NonNull GlideString group, @NonNull GlideString[] ids) { + GlideString[] args = concatenateArrays(new GlideString[] {key, group}, ids); + return commandManager.submitNewCommand(XAck, args, this::handleLongResponse); + } + + @Override + public CompletableFuture xpending(@NonNull String key, @NonNull String group) { + return commandManager.submitNewCommand( + XPending, new String[] {key, group}, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture xpending( + @NonNull GlideString key, @NonNull GlideString group) { + return commandManager.submitNewCommand( + XPending, new GlideString[] {key, group}, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture xpending( + @NonNull String key, + @NonNull String group, + @NonNull StreamRange start, + @NonNull StreamRange end, + long count) { + return xpending(key, group, start, end, count, StreamPendingOptions.builder().build()); + } + + @Override + public CompletableFuture xpending( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull StreamRange start, + @NonNull StreamRange end, + long count) { + return xpending(key, group, start, end, count, StreamPendingOptionsBinary.builder().build()); + } + + @Override + public CompletableFuture xpending( + @NonNull String key, + @NonNull String group, + @NonNull StreamRange start, + @NonNull StreamRange end, + long count, + @NonNull StreamPendingOptions options) { + String[] args = concatenateArrays(new String[] {key, group}, options.toArgs(start, end, count)); + return commandManager.submitNewCommand( + XPending, args, response -> castArray(handleArrayResponse(response), Object[].class)); + } + + @Override + public CompletableFuture xpending( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull StreamRange start, + @NonNull StreamRange end, + long count, + @NonNull StreamPendingOptionsBinary options) { + GlideString[] args = + concatenateArrays(new GlideString[] {key, group}, options.toArgs(start, end, count)); + return commandManager.submitNewCommand( + XPending, args, response -> castArray(handleArrayResponseBinary(response), Object[].class)); + } + + @Override + public CompletableFuture> xclaim( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String[] ids) { + String[] args = + concatenateArrays(new String[] {key, group, consumer, Long.toString(minIdleTime)}, ids); + return commandManager.submitNewCommand(XClaim, args, this::handleMapResponse); + } + + @Override + public CompletableFuture> xclaim( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString[] ids) { + GlideString[] args = + concatenateArrays( + new GlideString[] {key, group, consumer, gs(Long.toString(minIdleTime))}, ids); + return commandManager.submitNewCommand( + XClaim, + args, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> xclaim( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String[] ids, + @NonNull StreamClaimOptions options) { + String[] args = + concatenateArrays( + new String[] {key, group, consumer, Long.toString(minIdleTime)}, ids, options.toArgs()); + return commandManager.submitNewCommand(XClaim, args, this::handleMapResponse); + } + + @Override + public CompletableFuture> xclaim( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString[] ids, + @NonNull StreamClaimOptions options) { + String[] toArgsString = options.toArgs(); + GlideString[] toArgs = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] args = + concatenateArrays( + new GlideString[] {key, group, consumer, gs(Long.toString(minIdleTime))}, ids, toArgs); + return commandManager.submitNewCommand( + XClaim, + args, + response -> castMapOf2DArray(handleBinaryStringMapResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture xclaimJustId( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String[] ids) { + String[] args = + concatenateArrays( + new String[] {key, group, consumer, Long.toString(minIdleTime)}, + ids, + new String[] {JUST_ID_VALKEY_API}); + return commandManager.submitNewCommand( + XClaim, args, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture xclaimJustId( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString[] ids) { + GlideString[] args = + concatenateArrays( + new GlideString[] {key, group, consumer, gs(Long.toString(minIdleTime))}, + ids, + new GlideString[] {gs(JUST_ID_VALKEY_API)}); + return commandManager.submitNewCommand( + XClaim, + args, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture xclaimJustId( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String[] ids, + @NonNull StreamClaimOptions options) { + String[] args = + concatenateArrays( + new String[] {key, group, consumer, Long.toString(minIdleTime)}, + ids, + options.toArgs(), + new String[] {JUST_ID_VALKEY_API}); + return commandManager.submitNewCommand( + XClaim, args, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture xclaimJustId( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString[] ids, + @NonNull StreamClaimOptions options) { + String[] toArgsString = options.toArgs(); + GlideString[] toArgs = + Arrays.stream(toArgsString).map(GlideString::gs).toArray(GlideString[]::new); + GlideString[] args = + concatenateArrays( + new GlideString[] {key, group, consumer, gs(Long.toString(minIdleTime))}, + ids, + toArgs, + new GlideString[] {gs(JUST_ID_VALKEY_API)}); + return commandManager.submitNewCommand( + XClaim, + args, + response -> castArray(handleArrayResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture[]> xinfoGroups(@NonNull String key) { + return commandManager.submitNewCommand( + XInfoGroups, + new String[] {key}, + response -> castArray(handleArrayResponse(response), Map.class)); + } + + @Override + public CompletableFuture[]> xinfoGroups(@NonNull GlideString key) { + return commandManager.submitNewCommand( + XInfoGroups, + new GlideString[] {key}, + response -> castArray(handleArrayResponseBinary(response), Map.class)); + } + + @Override + public CompletableFuture[]> xinfoConsumers( + @NonNull String key, @NonNull String groupName) { + return commandManager.submitNewCommand( + XInfoConsumers, + new String[] {key, groupName}, + response -> castArray(handleArrayResponse(response), Map.class)); + } + + @Override + public CompletableFuture[]> xinfoConsumers( + @NonNull GlideString key, @NonNull GlideString groupName) { + return commandManager.submitNewCommand( + XInfoConsumers, + new GlideString[] {key, groupName}, + response -> castArray(handleArrayResponseBinary(response), Map.class)); + } + + @Override + public CompletableFuture xautoclaim( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String start) { + String[] args = new String[] {key, group, consumer, Long.toString(minIdleTime), start}; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponse); + } + + @Override + public CompletableFuture xautoclaim( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString start) { + GlideString[] args = + new GlideString[] {key, group, consumer, gs(Long.toString(minIdleTime)), start}; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponseBinary); + } + + @Override + public CompletableFuture xautoclaim( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String start, + long count) { + String[] args = + new String[] { + key, + group, + consumer, + Long.toString(minIdleTime), + start, + READ_COUNT_VALKEY_API, + Long.toString(count) + }; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponse); + } + + @Override + public CompletableFuture xautoclaim( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString start, + long count) { + GlideString[] args = + new GlideString[] { + key, + group, + consumer, + gs(Long.toString(minIdleTime)), + start, + gs(READ_COUNT_VALKEY_API), + gs(Long.toString(count)) + }; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponseBinary); + } + + @Override + public CompletableFuture xautoclaimJustId( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String start) { + String[] args = + new String[] {key, group, consumer, Long.toString(minIdleTime), start, JUST_ID_VALKEY_API}; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponse); + } + + @Override + public CompletableFuture xautoclaimJustId( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString start) { + GlideString[] args = + new GlideString[] { + key, group, consumer, gs(Long.toString(minIdleTime)), start, gs(JUST_ID_VALKEY_API) + }; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponseBinary); + } + + @Override + public CompletableFuture xautoclaimJustId( + @NonNull String key, + @NonNull String group, + @NonNull String consumer, + long minIdleTime, + @NonNull String start, + long count) { + String[] args = + new String[] { + key, + group, + consumer, + Long.toString(minIdleTime), + start, + READ_COUNT_VALKEY_API, + Long.toString(count), + JUST_ID_VALKEY_API + }; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponse); + } + + @Override + public CompletableFuture xautoclaimJustId( + @NonNull GlideString key, + @NonNull GlideString group, + @NonNull GlideString consumer, + long minIdleTime, + @NonNull GlideString start, + long count) { + GlideString[] args = + new GlideString[] { + key, + group, + consumer, + gs(Long.toString(minIdleTime)), + start, + gs(READ_COUNT_VALKEY_API), + gs(Long.toString(count)), + gs(JUST_ID_VALKEY_API) + }; + return commandManager.submitNewCommand(XAutoClaim, args, this::handleArrayResponseBinary); + } + + @Override + public CompletableFuture> xinfoStream(@NonNull String key) { + return commandManager.submitNewCommand( + XInfoStream, new String[] {key}, this::handleMapResponse); + } + + @Override + public CompletableFuture> xinfoStreamFull(@NonNull String key) { + return commandManager.submitNewCommand( + XInfoStream, new String[] {key, FULL}, response -> handleMapResponse(response)); + } + + @Override + public CompletableFuture> xinfoStreamFull(@NonNull String key, int count) { + return commandManager.submitNewCommand( + XInfoStream, + new String[] {key, FULL, COUNT, Integer.toString(count)}, + this::handleMapResponse); + } + + @Override + public CompletableFuture> xinfoStream(@NonNull GlideString key) { + return commandManager.submitNewCommand( + XInfoStream, new GlideString[] {key}, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> xinfoStreamFull(@NonNull GlideString key) { + return commandManager.submitNewCommand( + XInfoStream, new GlideString[] {key, gs(FULL)}, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> xinfoStreamFull( + @NonNull GlideString key, int count) { + return commandManager.submitNewCommand( + XInfoStream, + new GlideString[] {key, gs(FULL), gs(COUNT), gs(Integer.toString(count))}, + this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture pttl(@NonNull String key) { + return commandManager.submitNewCommand(PTTL, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture pttl(@NonNull GlideString key) { + return commandManager.submitNewCommand(PTTL, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture persist(@NonNull String key) { + return commandManager.submitNewCommand( + Persist, new String[] {key}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture persist(@NonNull GlideString key) { + return commandManager.submitNewCommand( + Persist, new GlideString[] {key}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture type(@NonNull String key) { + return commandManager.submitNewCommand(Type, new String[] {key}, this::handleStringResponse); + } + + @Override + public CompletableFuture type(@NonNull GlideString key) { + return commandManager.submitNewCommand( + Type, new GlideString[] {key}, this::handleStringResponse); + } + + @Override + public CompletableFuture linsert( + @NonNull String key, + @NonNull InsertPosition position, + @NonNull String pivot, + @NonNull String element) { + return commandManager.submitNewCommand( + LInsert, new String[] {key, position.toString(), pivot, element}, this::handleLongResponse); + } + + @Override + public CompletableFuture linsert( + @NonNull GlideString key, + @NonNull InsertPosition position, + @NonNull GlideString pivot, + @NonNull GlideString element) { + return commandManager.submitNewCommand( + LInsert, + new GlideString[] {key, gs(position.toString()), pivot, element}, + this::handleLongResponse); + } + + @Override + public CompletableFuture blpop(@NonNull String[] keys, double timeout) { + String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); + return commandManager.submitNewCommand( + BLPop, arguments, response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture blpop(@NonNull GlideString[] keys, double timeout) { + GlideString[] arguments = ArrayUtils.add(keys, gs(Double.toString(timeout))); + return commandManager.submitNewCommand( + BLPop, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture brpop(@NonNull String[] keys, double timeout) { + String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); + return commandManager.submitNewCommand( + BRPop, arguments, response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture brpop(@NonNull GlideString[] keys, double timeout) { + GlideString[] arguments = ArrayUtils.add(keys, gs(Double.toString(timeout))); + return commandManager.submitNewCommand( + BRPop, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture rpushx(@NonNull String key, @NonNull String[] elements) { + String[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture rpushx(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture lpushx(@NonNull String key, @NonNull String[] elements) { + String[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture lpushx(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture zrange( + @NonNull String key, @NonNull RangeQuery rangeQuery, boolean reverse) { + String[] arguments = RangeOptions.createZRangeArgs(key, rangeQuery, reverse, false); + + return commandManager.submitNewCommand( + ZRange, + arguments, + response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture zrange( + @NonNull GlideString key, @NonNull RangeQuery rangeQuery, boolean reverse) { + GlideString[] arguments = RangeOptions.createZRangeArgsBinary(key, rangeQuery, reverse, false); + + return commandManager.submitNewCommand( + ZRange, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture zrange(@NonNull String key, @NonNull RangeQuery rangeQuery) { + return zrange(key, rangeQuery, false); + } + + @Override + public CompletableFuture zrange( + @NonNull GlideString key, @NonNull RangeQuery rangeQuery) { + return zrange(key, rangeQuery, false); + } + + @Override + public CompletableFuture> zrangeWithScores( + @NonNull String key, @NonNull ScoredRangeQuery rangeQuery, boolean reverse) { + String[] arguments = RangeOptions.createZRangeArgs(key, rangeQuery, reverse, true); + + return commandManager.submitNewCommand(ZRange, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> zrangeWithScores( + @NonNull GlideString key, @NonNull ScoredRangeQuery rangeQuery, boolean reverse) { + GlideString[] arguments = RangeOptions.createZRangeArgsBinary(key, rangeQuery, reverse, true); + + return commandManager.submitNewCommand(ZRange, arguments, this::handleBinaryStringMapResponse); + } + + @Override + public CompletableFuture> zrangeWithScores( + @NonNull String key, @NonNull ScoredRangeQuery rangeQuery) { + return zrangeWithScores(key, rangeQuery, false); + } + + @Override + public CompletableFuture> zrangeWithScores( + @NonNull GlideString key, @NonNull ScoredRangeQuery rangeQuery) { + return zrangeWithScores(key, rangeQuery, false); + } + + @Override + public CompletableFuture zmpop(@NonNull String[] keys, @NonNull ScoreFilter modifier) { + String[] arguments = + concatenateArrays( + new String[] {Integer.toString(keys.length)}, keys, new String[] {modifier.toString()}); + return commandManager.submitNewCommand(ZMPop, arguments, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture zmpop( + @NonNull GlideString[] keys, @NonNull ScoreFilter modifier) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Integer.toString(keys.length))}, + keys, + new GlideString[] {gs(modifier.toString())}); + return commandManager.submitNewCommand(ZMPop, arguments, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture zmpop( + @NonNull String[] keys, @NonNull ScoreFilter modifier, long count) { + String[] arguments = + concatenateArrays( + new String[] {Integer.toString(keys.length)}, + keys, + new String[] {modifier.toString(), COUNT_VALKEY_API, Long.toString(count)}); + return commandManager.submitNewCommand(ZMPop, arguments, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture zmpop( + @NonNull GlideString[] keys, @NonNull ScoreFilter modifier, long count) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Integer.toString(keys.length))}, + keys, + new GlideString[] { + gs(modifier.toString()), gs(COUNT_VALKEY_API), gs(Long.toString(count)) + }); + return commandManager.submitNewCommand(ZMPop, arguments, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture bzmpop( + @NonNull String[] keys, @NonNull ScoreFilter modifier, double timeout) { + String[] arguments = + concatenateArrays( + new String[] {Double.toString(timeout), Integer.toString(keys.length)}, + keys, + new String[] {modifier.toString()}); + return commandManager.submitNewCommand(BZMPop, arguments, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture bzmpop( + @NonNull GlideString[] keys, @NonNull ScoreFilter modifier, double timeout) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Double.toString(timeout)), gs(Integer.toString(keys.length))}, + keys, + new GlideString[] {gs(modifier.toString())}); + return commandManager.submitNewCommand( + BZMPop, arguments, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture bzmpop( + @NonNull String[] keys, @NonNull ScoreFilter modifier, double timeout, long count) { + String[] arguments = + concatenateArrays( + new String[] {Double.toString(timeout), Integer.toString(keys.length)}, + keys, + new String[] {modifier.toString(), COUNT_VALKEY_API, Long.toString(count)}); + return commandManager.submitNewCommand(BZMPop, arguments, this::handleArrayOrNullResponse); + } + + @Override + public CompletableFuture bzmpop( + @NonNull GlideString[] keys, @NonNull ScoreFilter modifier, double timeout, long count) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Double.toString(timeout)), gs(Integer.toString(keys.length))}, + keys, + new GlideString[] { + gs(modifier.toString()), gs(COUNT_VALKEY_API), gs(Long.toString(count)) + }); + return commandManager.submitNewCommand( + BZMPop, arguments, this::handleArrayOrNullResponseBinary); + } + + @Override + public CompletableFuture pfadd(@NonNull String key, @NonNull String[] elements) { + String[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(PfAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture pfadd(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(PfAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture pfcount(@NonNull String[] keys) { + return commandManager.submitNewCommand(PfCount, keys, this::handleLongResponse); + } + + @Override + public CompletableFuture pfcount(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(PfCount, keys, this::handleLongResponse); + } + + @Override + public CompletableFuture pfmerge( + @NonNull String destination, @NonNull String[] sourceKeys) { + String[] arguments = ArrayUtils.addFirst(sourceKeys, destination); + return commandManager.submitNewCommand(PfMerge, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture pfmerge( + @NonNull GlideString destination, @NonNull GlideString[] sourceKeys) { + GlideString[] arguments = ArrayUtils.addFirst(sourceKeys, destination); + return commandManager.submitNewCommand(PfMerge, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture touch(@NonNull String[] keys) { + return commandManager.submitNewCommand(Touch, keys, this::handleLongResponse); + } + + @Override + public CompletableFuture touch(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(Touch, keys, this::handleLongResponse); + } + + @Override + public CompletableFuture geoadd( + @NonNull String key, + @NonNull Map membersToGeospatialData, + @NonNull GeoAddOptions options) { + String[] arguments = + concatenateArrays( + new String[] {key}, options.toArgs(), mapGeoDataToArray(membersToGeospatialData)); + return commandManager.submitNewCommand(GeoAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture geoadd( + @NonNull GlideString key, + @NonNull Map membersToGeospatialData, + @NonNull GeoAddOptions options) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(options.toArgs()) + .add(mapGeoDataToGlideStringArray(membersToGeospatialData)) + .toArray(); + + return commandManager.submitNewCommand(GeoAdd, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture geoadd( + @NonNull String key, @NonNull Map membersToGeospatialData) { + return geoadd(key, membersToGeospatialData, new GeoAddOptions(false)); + } + + @Override + public CompletableFuture geoadd( + @NonNull GlideString key, @NonNull Map membersToGeospatialData) { + return geoadd(key, membersToGeospatialData, new GeoAddOptions(false)); + } + + @Override + public CompletableFuture geopos(@NonNull String key, @NonNull String[] members) { + String[] arguments = concatenateArrays(new String[] {key}, members); + return commandManager.submitNewCommand( + GeoPos, + arguments, + response -> castArrayofArrays(handleArrayResponse(response), Double.class)); + } + + @Override + public CompletableFuture geopos( + @NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = concatenateArrays(new GlideString[] {key}, members); + return commandManager.submitNewCommand( + GeoPos, + arguments, + response -> castArrayofArrays(handleArrayResponse(response), Double.class)); + } + + @Override + public CompletableFuture geodist( + @NonNull String key, + @NonNull String member1, + @NonNull String member2, + @NonNull GeoUnit geoUnit) { + String[] arguments = new String[] {key, member1, member2, geoUnit.getValkeyAPI()}; + return commandManager.submitNewCommand(GeoDist, arguments, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture geodist( + @NonNull GlideString key, + @NonNull GlideString member1, + @NonNull GlideString member2, + @NonNull GeoUnit geoUnit) { + GlideString[] arguments = new GlideString[] {key, member1, member2, gs(geoUnit.getValkeyAPI())}; + return commandManager.submitNewCommand(GeoDist, arguments, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture geodist( + @NonNull String key, @NonNull String member1, @NonNull String member2) { + String[] arguments = new String[] {key, member1, member2}; + return commandManager.submitNewCommand(GeoDist, arguments, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture geodist( + @NonNull GlideString key, @NonNull GlideString member1, @NonNull GlideString member2) { + GlideString[] arguments = new GlideString[] {key, member1, member2}; + return commandManager.submitNewCommand(GeoDist, arguments, this::handleDoubleOrNullResponse); + } + + @Override + public CompletableFuture geohash(@NonNull String key, @NonNull String[] members) { + String[] arguments = concatenateArrays(new String[] {key}, members); + return commandManager.submitNewCommand( + GeoHash, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture geohash( + @NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = concatenateArrays(new GlideString[] {key}, members); + return commandManager.submitNewCommand( + GeoHash, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture bitcount(@NonNull String key) { + return commandManager.submitNewCommand(BitCount, new String[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture bitcount(@NonNull GlideString key) { + return commandManager.submitNewCommand( + BitCount, new GlideString[] {key}, this::handleLongResponse); + } + + @Override + public CompletableFuture bitcount(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + BitCount, + new String[] {key, Long.toString(start), Long.toString(end)}, + this::handleLongResponse); + } + + @Override + public CompletableFuture bitcount(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + BitCount, + new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}, + this::handleLongResponse); + } + + @Override + public CompletableFuture bitcount( + @NonNull String key, long start, long end, @NonNull BitmapIndexType options) { + String[] arguments = + new String[] {key, Long.toString(start), Long.toString(end), options.toString()}; + return commandManager.submitNewCommand(BitCount, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitcount( + @NonNull GlideString key, long start, long end, @NonNull BitmapIndexType options) { + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(start)), gs(Long.toString(end)), gs(options.toString()) + }; + return commandManager.submitNewCommand(BitCount, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture setbit(@NonNull String key, long offset, long value) { + String[] arguments = new String[] {key, Long.toString(offset), Long.toString(value)}; + return commandManager.submitNewCommand(SetBit, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture setbit(@NonNull GlideString key, long offset, long value) { + GlideString[] arguments = + new GlideString[] {key, gs(Long.toString(offset)), gs(Long.toString(value))}; + return commandManager.submitNewCommand(SetBit, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture getbit(@NonNull String key, long offset) { + String[] arguments = new String[] {key, Long.toString(offset)}; + return commandManager.submitNewCommand(GetBit, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture getbit(@NonNull GlideString key, long offset) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(offset))}; + return commandManager.submitNewCommand(GetBit, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos(@NonNull String key, long bit) { + String[] arguments = new String[] {key, Long.toString(bit)}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(bit))}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos(@NonNull String key, long bit, long start) { + String[] arguments = new String[] {key, Long.toString(bit), Long.toString(start)}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit, long start) { + GlideString[] arguments = + new GlideString[] {key, gs(Long.toString(bit)), gs(Long.toString(start))}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos(@NonNull String key, long bit, long start, long end) { + String[] arguments = + new String[] {key, Long.toString(bit), Long.toString(start), Long.toString(end)}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit, long start, long end) { + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(bit)), gs(Long.toString(start)), gs(Long.toString(end)) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos( + @NonNull String key, long bit, long start, long end, @NonNull BitmapIndexType options) { + String[] arguments = + new String[] { + key, Long.toString(bit), Long.toString(start), Long.toString(end), options.toString() + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitpos( + @NonNull GlideString key, long bit, long start, long end, @NonNull BitmapIndexType options) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit)), + gs(Long.toString(start)), + gs(Long.toString(end)), + gs(options.toString()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull String destination, + @NonNull String[] keys) { + String[] arguments = + concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys); + return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull GlideString destination, + @NonNull GlideString[] keys) { + GlideString[] arguments = + concatenateArrays(new GlideString[] {gs(bitwiseOperation.toString()), destination}, keys); + return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture> lmpop( + @NonNull String[] keys, @NonNull ListDirection direction, long count) { + String[] arguments = + concatenateArrays( + new String[] {Long.toString(keys.length)}, + keys, + new String[] {direction.toString(), COUNT_FOR_LIST_VALKEY_API, Long.toString(count)}); + return commandManager.submitNewCommand( + LMPop, + arguments, + response -> castMapOfArrays(handleMapOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture> lmpop( + @NonNull GlideString[] keys, @NonNull ListDirection direction, long count) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Long.toString(keys.length))}, + keys, + new GlideString[] { + gs(direction.toString()), gs(COUNT_FOR_LIST_VALKEY_API), gs(Long.toString(count)) + }); + return commandManager.submitNewCommand( + LMPop, + arguments, + response -> + castBinaryStringMapOfArrays( + handleBinaryStringMapOrNullResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> lmpop( + @NonNull String[] keys, @NonNull ListDirection direction) { + String[] arguments = + concatenateArrays( + new String[] {Long.toString(keys.length)}, keys, new String[] {direction.toString()}); + return commandManager.submitNewCommand( + LMPop, + arguments, + response -> castMapOfArrays(handleMapOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture> lmpop( + @NonNull GlideString[] keys, @NonNull ListDirection direction) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Long.toString(keys.length))}, + keys, + new GlideString[] {gs(direction.toString())}); + return commandManager.submitNewCommand( + LMPop, + arguments, + response -> + castBinaryStringMapOfArrays( + handleBinaryStringMapOrNullResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> blmpop( + @NonNull String[] keys, @NonNull ListDirection direction, long count, double timeout) { + String[] arguments = + concatenateArrays( + new String[] {Double.toString(timeout), Long.toString(keys.length)}, + keys, + new String[] {direction.toString(), COUNT_FOR_LIST_VALKEY_API, Long.toString(count)}); + return commandManager.submitNewCommand( + BLMPop, + arguments, + response -> castMapOfArrays(handleMapOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture> blmpop( + @NonNull GlideString[] keys, @NonNull ListDirection direction, long count, double timeout) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Double.toString(timeout)), gs(Long.toString(keys.length))}, + keys, + new GlideString[] { + gs(direction.toString()), gs(COUNT_FOR_LIST_VALKEY_API), gs(Long.toString(count)) + }); + return commandManager.submitNewCommand( + BLMPop, + arguments, + response -> + castBinaryStringMapOfArrays( + handleBinaryStringMapOrNullResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture> blmpop( + @NonNull String[] keys, @NonNull ListDirection direction, double timeout) { + String[] arguments = + concatenateArrays( + new String[] {Double.toString(timeout), Long.toString(keys.length)}, + keys, + new String[] {direction.toString()}); + return commandManager.submitNewCommand( + BLMPop, + arguments, + response -> castMapOfArrays(handleMapOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture> blmpop( + @NonNull GlideString[] keys, @NonNull ListDirection direction, double timeout) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Double.toString(timeout)), gs(Long.toString(keys.length))}, + keys, + new GlideString[] {gs(direction.toString())}); + return commandManager.submitNewCommand( + BLMPop, + arguments, + response -> + castBinaryStringMapOfArrays( + handleBinaryStringMapOrNullResponse(response), GlideString.class)); + } + + @Override + public CompletableFuture lset(@NonNull String key, long index, @NonNull String element) { + String[] arguments = new String[] {key, Long.toString(index), element}; + return commandManager.submitNewCommand(LSet, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lset( + @NonNull GlideString key, long index, @NonNull GlideString element) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(index)), element}; + return commandManager.submitNewCommand(LSet, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lmove( + @NonNull String source, + @NonNull String destination, + @NonNull ListDirection wherefrom, + @NonNull ListDirection whereto) { + String[] arguments = + new String[] {source, destination, wherefrom.toString(), whereto.toString()}; + return commandManager.submitNewCommand(LMove, arguments, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture lmove( + @NonNull GlideString source, + @NonNull GlideString destination, + @NonNull ListDirection wherefrom, + @NonNull ListDirection whereto) { + GlideString[] arguments = + new GlideString[] {source, destination, gs(wherefrom.toString()), gs(whereto.toString())}; + return commandManager.submitNewCommand(LMove, arguments, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture blmove( + @NonNull String source, + @NonNull String destination, + @NonNull ListDirection wherefrom, + @NonNull ListDirection whereto, + double timeout) { + String[] arguments = + new String[] { + source, destination, wherefrom.toString(), whereto.toString(), Double.toString(timeout) + }; + return commandManager.submitNewCommand(BLMove, arguments, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture blmove( + @NonNull GlideString source, + @NonNull GlideString destination, + @NonNull ListDirection wherefrom, + @NonNull ListDirection whereto, + double timeout) { + GlideString[] arguments = + new GlideString[] { + source, + destination, + gs(wherefrom.toString()), + gs(whereto.toString()), + gs(Double.toString(timeout)) + }; + return commandManager.submitNewCommand( + BLMove, arguments, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture srandmember(@NonNull String key) { + String[] arguments = new String[] {key}; + return commandManager.submitNewCommand( + SRandMember, arguments, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture srandmember(@NonNull GlideString key) { + GlideString[] arguments = new GlideString[] {key}; + return commandManager.submitNewCommand( + SRandMember, arguments, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture srandmember(@NonNull String key, long count) { + String[] arguments = new String[] {key, Long.toString(count)}; + return commandManager.submitNewCommand( + SRandMember, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture srandmember(@NonNull GlideString key, long count) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + return commandManager.submitNewCommand( + SRandMember, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture spop(@NonNull String key) { + String[] arguments = new String[] {key}; + return commandManager.submitNewCommand(SPop, arguments, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture spop(@NonNull GlideString key) { + GlideString[] arguments = new GlideString[] {key}; + return commandManager.submitNewCommand(SPop, arguments, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture> spopCount(@NonNull String key, long count) { + String[] arguments = new String[] {key, Long.toString(count)}; + return commandManager.submitNewCommand(SPop, arguments, this::handleSetResponse); + } + + @Override + public CompletableFuture> spopCount(@NonNull GlideString key, long count) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + return commandManager.submitNewCommand(SPop, arguments, this::handleSetBinaryResponse); + } + + @Override + public CompletableFuture bitfield( + @NonNull String key, @NonNull BitFieldSubCommands[] subCommands) { + String[] arguments = ArrayUtils.addFirst(createBitFieldArgs(subCommands), key); + return commandManager.submitNewCommand( + BitField, arguments, response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture bitfield( + @NonNull GlideString key, @NonNull BitFieldSubCommands[] subCommands) { + GlideString[] arguments = ArrayUtils.addFirst(createBitFieldGlideStringArgs(subCommands), key); + return commandManager.submitNewCommand( + BitField, arguments, response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture bitfieldReadOnly( + @NonNull String key, @NonNull BitFieldReadOnlySubCommands[] subCommands) { + String[] arguments = ArrayUtils.addFirst(createBitFieldArgs(subCommands), key); + return commandManager.submitNewCommand( + BitFieldReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture bitfieldReadOnly( + @NonNull GlideString key, @NonNull BitFieldReadOnlySubCommands[] subCommands) { + GlideString[] arguments = ArrayUtils.addFirst(createBitFieldGlideStringArgs(subCommands), key); + return commandManager.submitNewCommand( + BitFieldReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture sintercard(@NonNull String[] keys) { + String[] arguments = ArrayUtils.addFirst(keys, Long.toString(keys.length)); + return commandManager.submitNewCommand(SInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sintercard(@NonNull GlideString[] keys) { + GlideString[] arguments = new ArgsBuilder().add(keys.length).add(keys).toArray(); + return commandManager.submitNewCommand(SInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sintercard(@NonNull String[] keys, long limit) { + String[] arguments = + concatenateArrays( + new String[] {Long.toString(keys.length)}, + keys, + new String[] {SET_LIMIT_VALKEY_API, Long.toString(limit)}); + return commandManager.submitNewCommand(SInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sintercard(@NonNull GlideString[] keys, long limit) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Long.toString(keys.length))}, + keys, + new GlideString[] {gs(SET_LIMIT_VALKEY_API), gs(Long.toString(limit))}); + return commandManager.submitNewCommand(SInterCard, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture fcall( + @NonNull String function, @NonNull String[] keys, @NonNull String[] arguments) { + String[] args = + concatenateArrays(new String[] {function, Long.toString(keys.length)}, keys, arguments); + return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture fcall( + @NonNull GlideString function, + @NonNull GlideString[] keys, + @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays( + new GlideString[] {function, gs(Long.toString(keys.length))}, keys, arguments); + return commandManager.submitNewCommand(FCall, args, this::handleBinaryObjectOrNullResponse); + } + + @Override + public CompletableFuture fcallReadOnly( + @NonNull String function, @NonNull String[] keys, @NonNull String[] arguments) { + String[] args = + concatenateArrays(new String[] {function, Long.toString(keys.length)}, keys, arguments); + return commandManager.submitNewCommand(FCallReadOnly, args, this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture fcallReadOnly( + @NonNull GlideString function, + @NonNull GlideString[] keys, + @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays( + new GlideString[] {function, gs(Long.toString(keys.length))}, keys, arguments); return commandManager.submitNewCommand( - Expire, new String[] {key, Long.toString(seconds)}, this::handleBooleanResponse); + FCallReadOnly, args, this::handleBinaryObjectOrNullResponse); } @Override - public CompletableFuture expire( - @NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { - String[] arguments = - ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs()); - return commandManager.submitNewCommand(Expire, arguments, this::handleBooleanResponse); + public CompletableFuture copy( + @NonNull String source, @NonNull String destination, boolean replace) { + String[] arguments = new String[] {source, destination}; + if (replace) { + arguments = ArrayUtils.add(arguments, REPLACE_VALKEY_API); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } @Override - public CompletableFuture expireAt(@NonNull String key, long unixSeconds) { - return commandManager.submitNewCommand( - ExpireAt, new String[] {key, Long.toString(unixSeconds)}, this::handleBooleanResponse); + public CompletableFuture copy( + @NonNull GlideString source, @NonNull GlideString destination, boolean replace) { + GlideString[] arguments = new GlideString[] {source, destination}; + if (replace) { + arguments = ArrayUtils.add(arguments, gs(REPLACE_VALKEY_API)); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } @Override - public CompletableFuture expireAt( - @NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { - String[] arguments = - ArrayUtils.addAll(new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs()); - return commandManager.submitNewCommand(ExpireAt, arguments, this::handleBooleanResponse); + public CompletableFuture copy(@NonNull String source, @NonNull String destination) { + String[] arguments = new String[] {source, destination}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } @Override - public CompletableFuture pexpire(@NonNull String key, long milliseconds) { - return commandManager.submitNewCommand( - PExpire, new String[] {key, Long.toString(milliseconds)}, this::handleBooleanResponse); + public CompletableFuture copy( + @NonNull GlideString source, @NonNull GlideString destination) { + GlideString[] arguments = new GlideString[] {source, destination}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); } @Override - public CompletableFuture pexpire( - @NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { - String[] arguments = - ArrayUtils.addAll(new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs()); - return commandManager.submitNewCommand(PExpire, arguments, this::handleBooleanResponse); + public CompletableFuture msetnx(@NonNull Map keyValueMap) { + String[] args = convertMapToKeyValueStringArray(keyValueMap); + return commandManager.submitNewCommand(MSetNX, args, this::handleBooleanResponse); } @Override - public CompletableFuture pexpireAt(@NonNull String key, long unixMilliseconds) { - return commandManager.submitNewCommand( - PExpireAt, - new String[] {key, Long.toString(unixMilliseconds)}, - this::handleBooleanResponse); + public CompletableFuture msetnxBinary( + @NonNull Map keyValueMap) { + GlideString[] args = convertMapToKeyValueGlideStringArray(keyValueMap); + return commandManager.submitNewCommand(MSetNX, args, this::handleBooleanResponse); } @Override - public CompletableFuture pexpireAt( - @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { - String[] arguments = - ArrayUtils.addAll( - new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs()); - return commandManager.submitNewCommand(PExpireAt, arguments, this::handleBooleanResponse); + public CompletableFuture lcs(@NonNull String key1, @NonNull String key2) { + String[] arguments = new String[] {key1, key2}; + return commandManager.submitNewCommand(LCS, arguments, this::handleStringResponse); } @Override - public CompletableFuture ttl(@NonNull String key) { - return commandManager.submitNewCommand(TTL, new String[] {key}, this::handleLongResponse); + public CompletableFuture lcs(@NonNull GlideString key1, @NonNull GlideString key2) { + GlideString[] arguments = new GlideString[] {key1, key2}; + return commandManager.submitNewCommand(LCS, arguments, this::handleGlideStringResponse); } @Override - public CompletableFuture invokeScript(@NonNull Script script) { - return commandManager.submitScript( - script, List.of(), List.of(), this::handleObjectOrNullResponse); + public CompletableFuture lcsLen(@NonNull String key1, @NonNull String key2) { + String[] arguments = new String[] {key1, key2, LEN_VALKEY_API}; + return commandManager.submitNewCommand(LCS, arguments, this::handleLongResponse); } @Override - public CompletableFuture invokeScript( - @NonNull Script script, @NonNull ScriptOptions options) { - return commandManager.submitScript( - script, options.getKeys(), options.getArgs(), this::handleObjectOrNullResponse); + public CompletableFuture lcsLen(@NonNull GlideString key1, @NonNull GlideString key2) { + GlideString[] arguments = new ArgsBuilder().add(key1).add(key2).add(LEN_VALKEY_API).toArray(); + return commandManager.submitNewCommand(LCS, arguments, this::handleLongResponse); } @Override - public CompletableFuture zadd( - @NonNull String key, - @NonNull Map membersScoresMap, - @NonNull ZaddOptions options, - boolean changed) { - String[] changedArg = changed ? new String[] {"CH"} : new String[] {}; - String[] membersScores = convertMapToValueKeyStringArray(membersScoresMap); + public CompletableFuture> lcsIdx(@NonNull String key1, @NonNull String key2) { + String[] arguments = new String[] {key1, key2, IDX_COMMAND_STRING}; + return commandManager.submitNewCommand( + LCS, arguments, response -> handleLcsIdxResponse(handleMapResponse(response))); + } - String[] arguments = - concatenateArrays(new String[] {key}, options.toArgs(), changedArg, membersScores); + @Override + public CompletableFuture> lcsIdx( + @NonNull GlideString key1, @NonNull GlideString key2) { + GlideString[] arguments = + new ArgsBuilder().add(key1).add(key2).add(IDX_COMMAND_STRING).toArray(); - return commandManager.submitNewCommand(Zadd, arguments, this::handleLongResponse); + return commandManager.submitNewCommand( + LCS, arguments, response -> handleLcsIdxResponse(handleMapResponse(response))); } @Override - public CompletableFuture zadd( - @NonNull String key, - @NonNull Map membersScoresMap, - @NonNull ZaddOptions options) { - return this.zadd(key, membersScoresMap, options, false); + public CompletableFuture> lcsIdx( + @NonNull String key1, @NonNull String key2, long minMatchLen) { + String[] arguments = + new String[] { + key1, key2, IDX_COMMAND_STRING, MINMATCHLEN_COMMAND_STRING, String.valueOf(minMatchLen) + }; + return commandManager.submitNewCommand( + LCS, arguments, response -> handleLcsIdxResponse(handleMapResponse(response))); } @Override - public CompletableFuture zadd( - @NonNull String key, @NonNull Map membersScoresMap, boolean changed) { - return this.zadd(key, membersScoresMap, ZaddOptions.builder().build(), changed); + public CompletableFuture> lcsIdx( + @NonNull GlideString key1, @NonNull GlideString key2, long minMatchLen) { + GlideString[] arguments = + new ArgsBuilder() + .add(key1) + .add(key2) + .add(IDX_COMMAND_STRING) + .add(MINMATCHLEN_COMMAND_STRING) + .add(minMatchLen) + .toArray(); + return commandManager.submitNewCommand( + LCS, arguments, response -> handleLcsIdxResponse(handleMapResponse(response))); } @Override - public CompletableFuture zadd( - @NonNull String key, @NonNull Map membersScoresMap) { - return this.zadd(key, membersScoresMap, ZaddOptions.builder().build(), false); + public CompletableFuture> lcsIdxWithMatchLen( + @NonNull String key1, @NonNull String key2) { + String[] arguments = new String[] {key1, key2, IDX_COMMAND_STRING, WITHMATCHLEN_COMMAND_STRING}; + return commandManager.submitNewCommand(LCS, arguments, this::handleMapResponse); } @Override - public CompletableFuture zaddIncr( - @NonNull String key, @NonNull String member, double increment, @NonNull ZaddOptions options) { + public CompletableFuture> lcsIdxWithMatchLen( + @NonNull GlideString key1, @NonNull GlideString key2) { + GlideString[] arguments = + new ArgsBuilder() + .add(key1) + .add(key2) + .add(IDX_COMMAND_STRING) + .add(WITHMATCHLEN_COMMAND_STRING) + .toArray(); + return commandManager.submitNewCommand(LCS, arguments, this::handleMapResponse); + } + + @Override + public CompletableFuture> lcsIdxWithMatchLen( + @NonNull String key1, @NonNull String key2, long minMatchLen) { String[] arguments = concatenateArrays( - new String[] {key}, - options.toArgs(), - new String[] {"INCR", Double.toString(increment), member}); + new String[] { + key1, + key2, + IDX_COMMAND_STRING, + MINMATCHLEN_COMMAND_STRING, + String.valueOf(minMatchLen), + WITHMATCHLEN_COMMAND_STRING + }); + return commandManager.submitNewCommand(LCS, arguments, this::handleMapResponse); + } - return commandManager.submitNewCommand(Zadd, arguments, this::handleDoubleOrNullResponse); + @Override + public CompletableFuture> lcsIdxWithMatchLen( + @NonNull GlideString key1, @NonNull GlideString key2, long minMatchLen) { + GlideString[] arguments = + new ArgsBuilder() + .add(key1) + .add(key2) + .add(IDX_COMMAND_STRING) + .add(MINMATCHLEN_COMMAND_STRING) + .add(minMatchLen) + .add(WITHMATCHLEN_COMMAND_STRING) + .toArray(); + + return commandManager.submitNewCommand(LCS, arguments, this::handleMapResponse); } @Override - public CompletableFuture zaddIncr( - @NonNull String key, @NonNull String member, double increment) { - String[] arguments = - concatenateArrays( - new String[] {key}, new String[] {"INCR", Double.toString(increment), member}); + public CompletableFuture publish(@NonNull String message, @NonNull String channel) { + return commandManager.submitNewCommand( + Publish, + new String[] {channel, message}, + response -> { + // Check, but ignore the number - it is never valid. A GLIDE bug/limitation TODO + handleLongResponse(response); + return OK; + }); + } - return commandManager.submitNewCommand(Zadd, arguments, this::handleDoubleResponse); + @Override + public CompletableFuture publish( + @NonNull GlideString message, @NonNull GlideString channel) { + return commandManager.submitNewCommand( + Publish, + new GlideString[] {channel, message}, + response -> { + // Check, but ignore the number - it is never valid. A GLIDE bug/limitation TODO + handleLongResponse(response); + return OK; + }); } @Override - public CompletableFuture zrem(@NonNull String key, @NonNull String[] members) { - String[] arguments = ArrayUtils.addFirst(members, key); - return commandManager.submitNewCommand(Zrem, arguments, this::handleLongResponse); + public CompletableFuture watch(@NonNull String[] keys) { + return commandManager.submitNewCommand(Watch, keys, this::handleStringResponse); } @Override - public CompletableFuture zcard(@NonNull String key) { - return commandManager.submitNewCommand(Zcard, new String[] {key}, this::handleLongResponse); + public CompletableFuture watch(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(Watch, keys, this::handleStringResponse); } @Override - public CompletableFuture> zpopmin(@NonNull String key, long count) { - return commandManager.submitNewCommand( - ZPopMin, new String[] {key, Long.toString(count)}, this::handleMapResponse); + public CompletableFuture> sunion(@NonNull String[] keys) { + return commandManager.submitNewCommand(SUnion, keys, this::handleSetResponse); } @Override - public CompletableFuture> zpopmin(@NonNull String key) { - return commandManager.submitNewCommand(ZPopMin, new String[] {key}, this::handleMapResponse); + public CompletableFuture> sunion(@NonNull GlideString[] keys) { + return commandManager.submitNewCommand(SUnion, keys, this::handleSetBinaryResponse); + } + + // Hack: convert all `byte[]` -> `GlideString`. Better doing it here in the Java realm + // rather than doing it in the Rust code using JNI calls (performance) + private Object convertByteArrayToGlideString(Object o) { + if (o == null) return o; + + if (o instanceof byte[]) { + o = GlideString.of((byte[]) o); + } else if (o.getClass().isArray()) { + var array = (Object[]) o; + for (var i = 0; i < array.length; i++) { + array[i] = convertByteArrayToGlideString(array[i]); + } + } else if (o instanceof Set) { + var set = (Set) o; + o = set.stream().map(this::convertByteArrayToGlideString).collect(Collectors.toSet()); + } else if (o instanceof Map) { + var map = (Map) o; + o = + map.entrySet().stream() + .collect( + HashMap::new, + (m, e) -> + m.put( + convertByteArrayToGlideString(e.getKey()), + convertByteArrayToGlideString(e.getValue())), + HashMap::putAll); + } + return o; } @Override - public CompletableFuture> zpopmax(@NonNull String key, long count) { - return commandManager.submitNewCommand( - ZPopMax, new String[] {key, Long.toString(count)}, this::handleMapResponse); + public CompletableFuture dump(@NonNull GlideString key) { + GlideString[] arguments = new GlideString[] {key}; + return commandManager.submitNewCommand(Dump, arguments, this::handleBytesOrNullResponse); } @Override - public CompletableFuture> zpopmax(@NonNull String key) { - return commandManager.submitNewCommand(ZPopMax, new String[] {key}, this::handleMapResponse); + public CompletableFuture restore( + @NonNull GlideString key, long ttl, @NonNull byte[] value) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(ttl)), gs(value)}; + return commandManager.submitNewCommand(Restore, arguments, this::handleStringResponse); } @Override - public CompletableFuture zscore(@NonNull String key, @NonNull String member) { + public CompletableFuture restore( + @NonNull GlideString key, + long ttl, + @NonNull byte[] value, + @NonNull RestoreOptions restoreOptions) { + GlideString[] arguments = restoreOptions.toArgs(key, ttl, value); + return commandManager.submitNewCommand(Restore, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture sort(@NonNull String key) { return commandManager.submitNewCommand( - ZScore, new String[] {key, member}, this::handleDoubleOrNullResponse); + Sort, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); } @Override - public CompletableFuture zrank(@NonNull String key, @NonNull String member) { + public CompletableFuture sort(@NonNull GlideString key) { return commandManager.submitNewCommand( - Zrank, new String[] {key, member}, this::handleLongOrNullResponse); + Sort, + new GlideString[] {key}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override - public CompletableFuture zrankWithScore(@NonNull String key, @NonNull String member) { + public CompletableFuture sortReadOnly(@NonNull String key) { return commandManager.submitNewCommand( - Zrank, new String[] {key, member, WITH_SCORE_REDIS_API}, this::handleArrayOrNullResponse); + SortReadOnly, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); } @Override - public CompletableFuture zmscore(@NonNull String key, @NonNull String[] members) { - String[] arguments = ArrayUtils.addFirst(members, key); + public CompletableFuture sortReadOnly(@NonNull GlideString key) { return commandManager.submitNewCommand( - ZMScore, - arguments, - response -> castArray(handleArrayOrNullResponse(response), Double.class)); + SortReadOnly, + new GlideString[] {key}, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override - public CompletableFuture zdiff(@NonNull String[] keys) { - String[] arguments = ArrayUtils.addFirst(keys, Long.toString(keys.length)); + public CompletableFuture sortStore(@NonNull String key, @NonNull String destination) { return commandManager.submitNewCommand( - ZDiff, arguments, response -> castArray(handleArrayResponse(response), String.class)); + Sort, new String[] {key, STORE_COMMAND_STRING, destination}, this::handleLongResponse); } @Override - public CompletableFuture> zdiffWithScores(@NonNull String[] keys) { - String[] arguments = ArrayUtils.addFirst(keys, Long.toString(keys.length)); - arguments = ArrayUtils.add(arguments, WITH_SCORES_REDIS_API); - return commandManager.submitNewCommand(ZDiff, arguments, this::handleMapResponse); + public CompletableFuture sortStore( + @NonNull GlideString key, @NonNull GlideString destination) { + return commandManager.submitNewCommand( + Sort, + new GlideString[] {key, gs(STORE_COMMAND_STRING), destination}, + this::handleLongResponse); } @Override - public CompletableFuture zdiffstore(@NonNull String destination, @NonNull String[] keys) { + public CompletableFuture geosearch( + @NonNull String key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy) { String[] arguments = - ArrayUtils.addAll(new String[] {destination, Long.toString(keys.length)}, keys); - return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); + concatenateArrays(new String[] {key}, searchFrom.toArgs(), searchBy.toArgs()); + return commandManager.submitNewCommand( + GeoSearch, arguments, response -> castArray(handleArrayResponse(response), String.class)); } @Override - public CompletableFuture zcount( - @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + public CompletableFuture geosearch( + @NonNull GlideString key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(searchFrom.toArgs()).add(searchBy.toArgs()).toArray(); + return commandManager.submitNewCommand( - Zcount, new String[] {key, minScore.toArgs(), maxScore.toArgs()}, this::handleLongResponse); + GeoSearch, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override - public CompletableFuture zremrangebyrank(@NonNull String key, long start, long end) { + public CompletableFuture geosearch( + @NonNull String key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchResultOptions resultOptions) { + String[] arguments = + concatenateArrays( + new String[] {key}, searchFrom.toArgs(), searchBy.toArgs(), resultOptions.toArgs()); return commandManager.submitNewCommand( - ZRemRangeByRank, - new String[] {key, Long.toString(start), Long.toString(end)}, - this::handleLongResponse); + GeoSearch, arguments, response -> castArray(handleArrayResponse(response), String.class)); } @Override - public CompletableFuture zremrangebylex( - @NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + public CompletableFuture geosearch( + @NonNull GlideString key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchResultOptions resultOptions) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(resultOptions.toArgs()) + .toArray(); + return commandManager.submitNewCommand( - ZRemRangeByLex, - new String[] {key, minLex.toArgs(), maxLex.toArgs()}, - this::handleLongResponse); + GeoSearch, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); } @Override - public CompletableFuture zremrangebyscore( - @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + public CompletableFuture geosearch( + @NonNull String key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchOptions options) { + String[] arguments = + concatenateArrays( + new String[] {key}, searchFrom.toArgs(), searchBy.toArgs(), options.toArgs()); + return commandManager.submitNewCommand(GeoSearch, arguments, this::handleArrayResponse); + } + + @Override + public CompletableFuture geosearch( + @NonNull GlideString key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchOptions options) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()) + .toArray(); return commandManager.submitNewCommand( - ZRemRangeByScore, - new String[] {key, minScore.toArgs(), maxScore.toArgs()}, - this::handleLongResponse); + GeoSearch, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture zlexcount( - @NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + public CompletableFuture geosearch( + @NonNull String key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchOptions options, + @NonNull GeoSearchResultOptions resultOptions) { + String[] arguments = + concatenateArrays( + new String[] {key}, + searchFrom.toArgs(), + searchBy.toArgs(), + options.toArgs(), + resultOptions.toArgs()); + return commandManager.submitNewCommand(GeoSearch, arguments, this::handleArrayResponse); + } + + @Override + public CompletableFuture geosearch( + @NonNull GlideString key, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchOptions options, + @NonNull GeoSearchResultOptions resultOptions) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()) + .add(resultOptions.toArgs()) + .toArray(); return commandManager.submitNewCommand( - ZLexCount, new String[] {key, minLex.toArgs(), maxLex.toArgs()}, this::handleLongResponse); + GeoSearch, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture zrangestore( + public CompletableFuture geosearchstore( @NonNull String destination, @NonNull String source, - @NonNull RangeQuery rangeQuery, - boolean reverse) { + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy) { String[] arguments = - RangeOptions.createZRangeStoreArgs(destination, source, rangeQuery, reverse); + concatenateArrays( + new String[] {destination, source}, searchFrom.toArgs(), searchBy.toArgs()); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); + } - return commandManager.submitNewCommand(ZRangeStore, arguments, this::handleLongResponse); + @Override + public CompletableFuture geosearchstore( + @NonNull GlideString destination, + @NonNull GlideString source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy) { + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .toArray(); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture zrangestore( - @NonNull String destination, @NonNull String source, @NonNull RangeQuery rangeQuery) { - return this.zrangestore(destination, source, rangeQuery, false); + public CompletableFuture geosearchstore( + @NonNull String destination, + @NonNull String source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchResultOptions resultOptions) { + String[] arguments = + concatenateArrays( + new String[] {destination, source}, + searchFrom.toArgs(), + searchBy.toArgs(), + resultOptions.toArgs()); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture xadd(@NonNull String key, @NonNull Map values) { - return xadd(key, values, StreamAddOptions.builder().build()); + public CompletableFuture geosearchstore( + @NonNull GlideString destination, + @NonNull GlideString source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchResultOptions resultOptions) { + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(resultOptions.toArgs()) + .toArray(); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture xadd( - @NonNull String key, @NonNull Map values, @NonNull StreamAddOptions options) { + public CompletableFuture geosearchstore( + @NonNull String destination, + @NonNull String source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchStoreOptions options) { String[] arguments = - ArrayUtils.addAll( - ArrayUtils.addFirst(options.toArgs(), key), convertMapToKeyValueStringArray(values)); - return commandManager.submitNewCommand(XAdd, arguments, this::handleStringOrNullResponse); + concatenateArrays( + new String[] {destination, source}, + searchFrom.toArgs(), + searchBy.toArgs(), + options.toArgs()); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture pttl(@NonNull String key) { - return commandManager.submitNewCommand(PTTL, new String[] {key}, this::handleLongResponse); + public CompletableFuture geosearchstore( + @NonNull GlideString destination, + @NonNull GlideString source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchStoreOptions options) { + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()) + .toArray(); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture persist(@NonNull String key) { - return commandManager.submitNewCommand( - Persist, new String[] {key}, this::handleBooleanResponse); + public CompletableFuture geosearchstore( + @NonNull String destination, + @NonNull String source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchStoreOptions options, + @NonNull GeoSearchResultOptions resultOptions) { + String[] arguments = + concatenateArrays( + new String[] {destination, source}, + searchFrom.toArgs(), + searchBy.toArgs(), + options.toArgs(), + resultOptions.toArgs()); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture type(@NonNull String key) { - return commandManager.submitNewCommand(Type, new String[] {key}, this::handleStringResponse); + public CompletableFuture geosearchstore( + @NonNull GlideString destination, + @NonNull GlideString source, + @NonNull GeoSearchOrigin.SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchStoreOptions options, + @NonNull GeoSearchResultOptions resultOptions) { + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()) + .add(resultOptions.toArgs()) + .toArray(); + return commandManager.submitNewCommand(GeoSearchStore, arguments, this::handleLongResponse); } @Override - public CompletableFuture linsert( - @NonNull String key, - @NonNull InsertPosition position, - @NonNull String pivot, - @NonNull String element) { - return commandManager.submitNewCommand( - LInsert, new String[] {key, position.toString(), pivot, element}, this::handleLongResponse); + public CompletableFuture sscan(@NonNull String key, @NonNull String cursor) { + String[] arguments = new String[] {key, cursor}; + return commandManager.submitNewCommand(SScan, arguments, this::handleArrayResponse); } @Override - public CompletableFuture blpop(@NonNull String[] keys, double timeout) { - String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); - return commandManager.submitNewCommand( - Blpop, arguments, response -> castArray(handleArrayOrNullResponse(response), String.class)); + public CompletableFuture sscan(@NonNull GlideString key, @NonNull GlideString cursor) { + GlideString[] arguments = new GlideString[] {key, cursor}; + return commandManager.submitNewCommand(SScan, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture brpop(@NonNull String[] keys, double timeout) { - String[] arguments = ArrayUtils.add(keys, Double.toString(timeout)); - return commandManager.submitNewCommand( - Brpop, arguments, response -> castArray(handleArrayOrNullResponse(response), String.class)); + public CompletableFuture sscan( + @NonNull String key, @NonNull String cursor, @NonNull SScanOptions sScanOptions) { + String[] arguments = concatenateArrays(new String[] {key, cursor}, sScanOptions.toArgs()); + return commandManager.submitNewCommand(SScan, arguments, this::handleArrayResponse); } @Override - public CompletableFuture rpushx(@NonNull String key, @NonNull String[] elements) { - String[] arguments = ArrayUtils.addFirst(elements, key); - return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); + public CompletableFuture sscan( + @NonNull GlideString key, + @NonNull GlideString cursor, + @NonNull SScanOptionsBinary sScanOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(cursor).add(sScanOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand(SScan, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture lpushx(@NonNull String key, @NonNull String[] elements) { - String[] arguments = ArrayUtils.addFirst(elements, key); - return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); + public CompletableFuture zscan(@NonNull String key, @NonNull String cursor) { + String[] arguments = new String[] {key, cursor}; + return commandManager.submitNewCommand(ZScan, arguments, this::handleArrayResponse); } @Override - public CompletableFuture zrange( - @NonNull String key, @NonNull RangeQuery rangeQuery, boolean reverse) { - String[] arguments = RangeOptions.createZRangeArgs(key, rangeQuery, reverse, false); - - return commandManager.submitNewCommand( - Zrange, - arguments, - response -> castArray(handleArrayOrNullResponse(response), String.class)); + public CompletableFuture zscan(@NonNull GlideString key, @NonNull GlideString cursor) { + GlideString[] arguments = new GlideString[] {key, cursor}; + return commandManager.submitNewCommand(ZScan, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture zrange(@NonNull String key, @NonNull RangeQuery rangeQuery) { - return this.zrange(key, rangeQuery, false); + public CompletableFuture zscan( + @NonNull String key, @NonNull String cursor, @NonNull ZScanOptions zScanOptions) { + String[] arguments = concatenateArrays(new String[] {key, cursor}, zScanOptions.toArgs()); + return commandManager.submitNewCommand(ZScan, arguments, this::handleArrayResponse); } @Override - public CompletableFuture> zrangeWithScores( - @NonNull String key, @NonNull ScoredRangeQuery rangeQuery, boolean reverse) { - String[] arguments = RangeOptions.createZRangeArgs(key, rangeQuery, reverse, true); + public CompletableFuture zscan( + @NonNull GlideString key, + @NonNull GlideString cursor, + @NonNull ZScanOptionsBinary zScanOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(cursor).add(zScanOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand(ZScan, arguments, this::handleArrayOrNullResponseBinary); + } - return commandManager.submitNewCommand(Zrange, arguments, this::handleMapResponse); + @Override + public CompletableFuture hscan(@NonNull String key, @NonNull String cursor) { + String[] arguments = new String[] {key, cursor}; + return commandManager.submitNewCommand(HScan, arguments, this::handleArrayResponse); } @Override - public CompletableFuture> zrangeWithScores( - @NonNull String key, @NonNull ScoredRangeQuery rangeQuery) { - return this.zrangeWithScores(key, rangeQuery, false); + public CompletableFuture hscan(@NonNull GlideString key, @NonNull GlideString cursor) { + GlideString[] arguments = new GlideString[] {key, cursor}; + return commandManager.submitNewCommand(HScan, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture pfadd(@NonNull String key, @NonNull String[] elements) { - String[] arguments = ArrayUtils.addFirst(elements, key); - return commandManager.submitNewCommand(PfAdd, arguments, this::handleLongResponse); + public CompletableFuture hscan( + @NonNull String key, @NonNull String cursor, @NonNull HScanOptions hScanOptions) { + String[] arguments = concatenateArrays(new String[] {key, cursor}, hScanOptions.toArgs()); + return commandManager.submitNewCommand(HScan, arguments, this::handleArrayResponse); } @Override - public CompletableFuture pfcount(@NonNull String[] keys) { - return commandManager.submitNewCommand(PfCount, keys, this::handleLongResponse); + public CompletableFuture hscan( + @NonNull GlideString key, + @NonNull GlideString cursor, + @NonNull HScanOptionsBinary hScanOptions) { + GlideString[] arguments = + new ArgsBuilder().add(key).add(cursor).add(hScanOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand(HScan, arguments, this::handleArrayOrNullResponseBinary); } @Override - public CompletableFuture pfmerge( - @NonNull String destination, @NonNull String[] sourceKeys) { - String[] arguments = ArrayUtils.addFirst(sourceKeys, destination); - return commandManager.submitNewCommand(PfMerge, arguments, this::handleStringResponse); + public CompletableFuture wait(long numreplicas, long timeout) { + return commandManager.submitNewCommand( + Wait, + new String[] {Long.toString(numreplicas), Long.toString(timeout)}, + this::handleLongResponse); } } diff --git a/java/client/src/main/java/glide/api/GlideClient.java b/java/client/src/main/java/glide/api/GlideClient.java new file mode 100644 index 0000000000..53eaeb369d --- /dev/null +++ b/java/client/src/main/java/glide/api/GlideClient.java @@ -0,0 +1,560 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api; + +import static command_request.CommandRequestOuterClass.RequestType.ClientGetName; +import static command_request.CommandRequestOuterClass.RequestType.ClientId; +import static command_request.CommandRequestOuterClass.RequestType.ConfigGet; +import static command_request.CommandRequestOuterClass.RequestType.ConfigResetStat; +import static command_request.CommandRequestOuterClass.RequestType.ConfigRewrite; +import static command_request.CommandRequestOuterClass.RequestType.ConfigSet; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.CustomCommand; +import static command_request.CommandRequestOuterClass.RequestType.DBSize; +import static command_request.CommandRequestOuterClass.RequestType.Echo; +import static command_request.CommandRequestOuterClass.RequestType.FlushAll; +import static command_request.CommandRequestOuterClass.RequestType.FlushDB; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDelete; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDump; +import static command_request.CommandRequestOuterClass.RequestType.FunctionFlush; +import static command_request.CommandRequestOuterClass.RequestType.FunctionKill; +import static command_request.CommandRequestOuterClass.RequestType.FunctionList; +import static command_request.CommandRequestOuterClass.RequestType.FunctionLoad; +import static command_request.CommandRequestOuterClass.RequestType.FunctionRestore; +import static command_request.CommandRequestOuterClass.RequestType.FunctionStats; +import static command_request.CommandRequestOuterClass.RequestType.Info; +import static command_request.CommandRequestOuterClass.RequestType.LastSave; +import static command_request.CommandRequestOuterClass.RequestType.Lolwut; +import static command_request.CommandRequestOuterClass.RequestType.Move; +import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.RandomKey; +import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.Select; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Time; +import static command_request.CommandRequestOuterClass.RequestType.UnWatch; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_VALKEY_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_VALKEY_API; +import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; +import static glide.utils.ArrayTransformUtils.castArray; +import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; + +import glide.api.commands.ConnectionManagementCommands; +import glide.api.commands.GenericCommands; +import glide.api.commands.ScriptingAndFunctionsCommands; +import glide.api.commands.ServerManagementCommands; +import glide.api.commands.TransactionsCommands; +import glide.api.models.GlideString; +import glide.api.models.Transaction; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.SortOptionsBinary; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.utils.ArgsBuilder; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; + +/** + * Async (non-blocking) client for Standalone mode. Use {@link #createClient} to request a client. + */ +public class GlideClient extends BaseClient + implements GenericCommands, + ServerManagementCommands, + ConnectionManagementCommands, + ScriptingAndFunctionsCommands, + TransactionsCommands { + + /** + * A constructor. Use {@link #createClient} to get a client. Made protected to simplify testing. + */ + protected GlideClient(ClientBuilder builder) { + super(builder); + } + + /** + * Async request for an async (non-blocking) client in Standalone mode. + * + * @param config Glide client Configuration. + * @return A Future to connect and return a GlideClient. + */ + public static CompletableFuture createClient( + @NonNull GlideClientConfiguration config) { + return createClient(config, GlideClient::new); + } + + @Override + public CompletableFuture customCommand(@NonNull String[] args) { + return commandManager.submitNewCommand(CustomCommand, args, this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture exec(@NonNull Transaction transaction) { + if (transaction.isBinaryOutput()) { + return commandManager.submitNewTransaction( + transaction, this::handleArrayOrNullResponseBinary); + } else { + return commandManager.submitNewTransaction(transaction, this::handleArrayOrNullResponse); + } + } + + @Override + public CompletableFuture ping() { + return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull String message) { + return commandManager.submitNewCommand( + Ping, new String[] {message}, this::handleStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull GlideString message) { + return commandManager.submitNewCommand( + Ping, new GlideString[] {message}, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture info() { + return commandManager.submitNewCommand(Info, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture info(@NonNull InfoOptions options) { + return commandManager.submitNewCommand(Info, options.toArgs(), this::handleStringResponse); + } + + @Override + public CompletableFuture select(long index) { + return commandManager.submitNewCommand( + Select, new String[] {Long.toString(index)}, this::handleStringResponse); + } + + @Override + public CompletableFuture clientId() { + return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture clientGetName() { + return commandManager.submitNewCommand( + ClientGetName, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture configRewrite() { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat() { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture> configGet(@NonNull String[] parameters) { + return commandManager.submitNewCommand(ConfigGet, parameters, this::handleMapResponse); + } + + @Override + public CompletableFuture configSet(@NonNull Map parameters) { + return commandManager.submitNewCommand( + ConfigSet, convertMapToKeyValueStringArray(parameters), this::handleStringResponse); + } + + @Override + public CompletableFuture echo(@NonNull String message) { + return commandManager.submitNewCommand( + Echo, new String[] {message}, this::handleStringResponse); + } + + @Override + public CompletableFuture echo(@NonNull GlideString message) { + return commandManager.submitNewCommand( + Echo, new GlideString[] {message}, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture time() { + return commandManager.submitNewCommand( + Time, new String[0], response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture lastsave() { + return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture flushall() { + return commandManager.submitNewCommand(FlushAll, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture flushall(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FlushAll, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture flushdb() { + return commandManager.submitNewCommand(FlushDB, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture flushdb(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FlushDB, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut() { + return commandManager.submitNewCommand(Lolwut, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int @NonNull [] parameters) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + return commandManager.submitNewCommand(Lolwut, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version) { + return commandManager.submitNewCommand( + Lolwut, + new String[] {VERSION_VALKEY_API, Integer.toString(version)}, + this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version, int @NonNull [] parameters) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_VALKEY_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + return commandManager.submitNewCommand(Lolwut, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture dbsize() { + return commandManager.submitNewCommand(DBSize, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture functionLoad(@NonNull String libraryCode, boolean replace) { + String[] arguments = + replace ? new String[] {REPLACE.toString(), libraryCode} : new String[] {libraryCode}; + return commandManager.submitNewCommand(FunctionLoad, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture functionLoad( + @NonNull GlideString libraryCode, boolean replace) { + GlideString[] arguments = + replace + ? new GlideString[] {gs(REPLACE.toString()), libraryCode} + : new GlideString[] {libraryCode}; + return commandManager.submitNewCommand( + FunctionLoad, arguments, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture move(@NonNull String key, long dbIndex) { + return commandManager.submitNewCommand( + Move, new String[] {key, Long.toString(dbIndex)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture move(@NonNull GlideString key, long dbIndex) { + return commandManager.submitNewCommand( + Move, new GlideString[] {key, gs(Long.toString(dbIndex))}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture[]> functionList(boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode ? new String[] {WITH_CODE_VALKEY_API} : new String[0], + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]> functionListBinary(boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + new ArgsBuilder().addIf(WITH_CODE_VALKEY_API, withCode).toArray(), + response -> handleFunctionListResponseBinary(handleArrayResponseBinary(response))); + } + + @Override + public CompletableFuture[]> functionList( + @NonNull String libNamePattern, boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode + ? new String[] {LIBRARY_NAME_VALKEY_API, libNamePattern, WITH_CODE_VALKEY_API} + : new String[] {LIBRARY_NAME_VALKEY_API, libNamePattern}, + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]> functionListBinary( + @NonNull GlideString libNamePattern, boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + new ArgsBuilder() + .add(LIBRARY_NAME_VALKEY_API) + .add(libNamePattern) + .addIf(WITH_CODE_VALKEY_API, withCode) + .toArray(), + response -> handleFunctionListResponseBinary(handleArrayResponseBinary(response))); + } + + @Override + public CompletableFuture functionFlush() { + return commandManager.submitNewCommand( + FunctionFlush, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FunctionFlush, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete(@NonNull String libName) { + return commandManager.submitNewCommand( + FunctionDelete, new String[] {libName}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete(@NonNull GlideString libName) { + return commandManager.submitNewCommand( + FunctionDelete, new GlideString[] {libName}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDump() { + return commandManager.submitNewCommand( + FunctionDump, new GlideString[0], this::handleBytesOrNullResponse); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + this::handleStringResponse); + } + + @Override + public CompletableFuture fcall(@NonNull String function) { + return fcall(function, new String[0], new String[0]); + } + + @Override + public CompletableFuture fcall(@NonNull GlideString function) { + return fcall(function, new GlideString[0], new GlideString[0]); + } + + @Override + public CompletableFuture fcallReadOnly(@NonNull String function) { + return fcallReadOnly(function, new String[0], new String[0]); + } + + @Override + public CompletableFuture fcallReadOnly(@NonNull GlideString function) { + return fcallReadOnly(function, new GlideString[0], new GlideString[0]); + } + + @Override + public CompletableFuture copy( + @NonNull String source, @NonNull String destination, long destinationDB) { + String[] arguments = + new String[] {source, destination, DB_VALKEY_API, Long.toString(destinationDB)}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture copy( + @NonNull GlideString source, @NonNull GlideString destination, long destinationDB) { + GlideString[] arguments = + new GlideString[] { + source, destination, gs(DB_VALKEY_API), gs(Long.toString(destinationDB)) + }; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture copy( + @NonNull String source, @NonNull String destination, long destinationDB, boolean replace) { + String[] arguments = + new String[] {source, destination, DB_VALKEY_API, Long.toString(destinationDB)}; + if (replace) { + arguments = ArrayUtils.add(arguments, REPLACE_VALKEY_API); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture copy( + @NonNull GlideString source, + @NonNull GlideString destination, + long destinationDB, + boolean replace) { + GlideString[] arguments = + new GlideString[] { + source, destination, gs(DB_VALKEY_API), gs(Long.toString(destinationDB)) + }; + if (replace) { + arguments = ArrayUtils.add(arguments, gs(REPLACE_VALKEY_API)); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture functionKill() { + return commandManager.submitNewCommand(FunctionKill, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture>> functionStats() { + return commandManager.submitNewCommand( + FunctionStats, + new String[0], + response -> handleFunctionStatsResponse(handleMapResponse(response))); + } + + @Override + public CompletableFuture>> functionStatsBinary() { + return commandManager.submitNewCommand( + FunctionStats, + new GlideString[0], + response -> handleFunctionStatsBinaryResponse(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture unwatch() { + return commandManager.submitNewCommand(UnWatch, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture randomKey() { + return commandManager.submitNewCommand( + RandomKey, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture randomKeyBinary() { + return commandManager.submitNewCommand( + RandomKey, new GlideString[0], this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture sort(@NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sort( + @NonNull GlideString key, @NonNull SortOptionsBinary sortOptions) { + GlideString[] arguments = new ArgsBuilder().add(key).add(sortOptions.toArgs()).toArray(); + return commandManager.submitNewCommand( + Sort, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull GlideString key, @NonNull SortOptionsBinary sortOptions) { + GlideString[] arguments = new ArgsBuilder().add(key).add(sortOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sortStore( + @NonNull GlideString key, + @NonNull GlideString destination, + @NonNull SortOptionsBinary sortOptions) { + + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(sortOptions.toArgs()) + .add(STORE_COMMAND_STRING) + .add(destination) + .toArray(); + + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture scan(@NonNull String cursor) { + return commandManager.submitNewCommand(Scan, new String[] {cursor}, this::handleArrayResponse); + } + + @Override + public CompletableFuture scan(@NonNull GlideString cursor) { + return commandManager.submitNewCommand( + Scan, new GlideString[] {cursor}, this::handleArrayResponseBinary); + } + + @Override + public CompletableFuture scan(@NonNull String cursor, @NonNull ScanOptions options) { + String[] arguments = ArrayUtils.addFirst(options.toArgs(), cursor); + return commandManager.submitNewCommand(Scan, arguments, this::handleArrayResponse); + } + + @Override + public CompletableFuture scan( + @NonNull GlideString cursor, @NonNull ScanOptions options) { + GlideString[] arguments = new ArgsBuilder().add(cursor).add(options.toArgs()).toArray(); + return commandManager.submitNewCommand(Scan, arguments, this::handleArrayResponseBinary); + } +} diff --git a/java/client/src/main/java/glide/api/GlideClusterClient.java b/java/client/src/main/java/glide/api/GlideClusterClient.java new file mode 100644 index 0000000000..65b8721ec8 --- /dev/null +++ b/java/client/src/main/java/glide/api/GlideClusterClient.java @@ -0,0 +1,1210 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api; + +import static command_request.CommandRequestOuterClass.RequestType.ClientGetName; +import static command_request.CommandRequestOuterClass.RequestType.ClientId; +import static command_request.CommandRequestOuterClass.RequestType.ConfigGet; +import static command_request.CommandRequestOuterClass.RequestType.ConfigResetStat; +import static command_request.CommandRequestOuterClass.RequestType.ConfigRewrite; +import static command_request.CommandRequestOuterClass.RequestType.ConfigSet; +import static command_request.CommandRequestOuterClass.RequestType.CustomCommand; +import static command_request.CommandRequestOuterClass.RequestType.DBSize; +import static command_request.CommandRequestOuterClass.RequestType.Echo; +import static command_request.CommandRequestOuterClass.RequestType.FCall; +import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.FlushAll; +import static command_request.CommandRequestOuterClass.RequestType.FlushDB; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDelete; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDump; +import static command_request.CommandRequestOuterClass.RequestType.FunctionFlush; +import static command_request.CommandRequestOuterClass.RequestType.FunctionKill; +import static command_request.CommandRequestOuterClass.RequestType.FunctionList; +import static command_request.CommandRequestOuterClass.RequestType.FunctionLoad; +import static command_request.CommandRequestOuterClass.RequestType.FunctionRestore; +import static command_request.CommandRequestOuterClass.RequestType.FunctionStats; +import static command_request.CommandRequestOuterClass.RequestType.Info; +import static command_request.CommandRequestOuterClass.RequestType.LastSave; +import static command_request.CommandRequestOuterClass.RequestType.Lolwut; +import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.RandomKey; +import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Time; +import static command_request.CommandRequestOuterClass.RequestType.UnWatch; +import static glide.api.commands.ServerManagementCommands.VERSION_VALKEY_API; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_VALKEY_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_VALKEY_API; +import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; +import static glide.utils.ArrayTransformUtils.castArray; +import static glide.utils.ArrayTransformUtils.castMapOfArrays; +import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; + +import glide.api.commands.ConnectionManagementClusterCommands; +import glide.api.commands.GenericClusterCommands; +import glide.api.commands.PubSubClusterCommands; +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.commands.ServerManagementClusterCommands; +import glide.api.commands.TransactionsClusterCommands; +import glide.api.logging.Logger; +import glide.api.models.ClusterTransaction; +import glide.api.models.ClusterValue; +import glide.api.models.GlideString; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.scan.ClusterScanCursor; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.GlideClusterClientConfiguration; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; +import glide.ffi.resolvers.ClusterScanCursorResolver; +import glide.managers.CommandManager; +import glide.utils.ArgsBuilder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; +import response.ResponseOuterClass.Response; + +/** Async (non-blocking) client for Cluster mode. Use {@link #createClient} to request a client. */ +public class GlideClusterClient extends BaseClient + implements ConnectionManagementClusterCommands, + GenericClusterCommands, + ServerManagementClusterCommands, + ScriptingAndFunctionsClusterCommands, + TransactionsClusterCommands, + PubSubClusterCommands { + + /** A private constructor. Use {@link #createClient} to get a client. */ + GlideClusterClient(ClientBuilder builder) { + super(builder); + } + + /** + * Async request for an async (non-blocking) client in Cluster mode. + * + * @param config Glide cluster client Configuration. + * @return A Future to connect and return a GlideClusterClient. + */ + public static CompletableFuture createClient( + @NonNull GlideClusterClientConfiguration config) { + return createClient(config, GlideClusterClient::new); + } + + @Override + public CompletableFuture> customCommand(@NonNull String[] args) { + // TODO if a command returns a map as a single value, ClusterValue misleads user + return commandManager.submitNewCommand( + CustomCommand, args, response -> ClusterValue.of(handleObjectOrNullResponse(response))); + } + + @Override + public CompletableFuture> customCommand( + @NonNull String[] args, @NonNull Route route) { + return commandManager.submitNewCommand( + CustomCommand, args, route, response -> handleCustomCommandResponse(route, response)); + } + + protected ClusterValue handleCustomCommandResponse(Route route, Response response) { + if (route instanceof SingleNodeRoute) { + return ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)); + } + if (response.hasConstantResponse()) { + return ClusterValue.ofSingleValue(handleStringResponse(response)); + } + return ClusterValue.ofMultiValue(handleMapResponse(response)); + } + + @Override + public CompletableFuture exec(@NonNull ClusterTransaction transaction) { + if (transaction.isBinaryOutput()) { + return commandManager.submitNewTransaction( + transaction, Optional.empty(), this::handleArrayOrNullResponseBinary); + } else { + return commandManager.submitNewTransaction( + transaction, Optional.empty(), this::handleArrayOrNullResponse); + } + } + + @Override + public CompletableFuture exec( + @NonNull ClusterTransaction transaction, @NonNull SingleNodeRoute route) { + if (transaction.isBinaryOutput()) { + return commandManager.submitNewTransaction( + transaction, Optional.of(route), this::handleArrayOrNullResponseBinary); + } else { + return commandManager.submitNewTransaction( + transaction, Optional.of(route), this::handleArrayOrNullResponse); + } + } + + @Override + public CompletableFuture ping() { + return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull String message) { + return commandManager.submitNewCommand( + Ping, new String[] {message}, this::handleStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull GlideString message) { + return commandManager.submitNewCommand( + Ping, new GlideString[] {message}, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull Route route) { + return commandManager.submitNewCommand(Ping, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull String message, @NonNull Route route) { + return commandManager.submitNewCommand( + Ping, new String[] {message}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture ping(@NonNull GlideString message, @NonNull Route route) { + return commandManager.submitNewCommand( + Ping, new GlideString[] {message}, route, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture> info() { + return commandManager.submitNewCommand( + Info, new String[0], response -> ClusterValue.of(handleMapResponse(response))); + } + + public CompletableFuture> info(@NonNull Route route) { + return commandManager.submitNewCommand( + Info, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleStringResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture> info(@NonNull InfoOptions options) { + return commandManager.submitNewCommand( + Info, options.toArgs(), response -> ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture> info( + @NonNull InfoOptions options, @NonNull Route route) { + return commandManager.submitNewCommand( + Info, + options.toArgs(), + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleStringResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture clientId() { + return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture> clientId(@NonNull Route route) { + return commandManager.submitNewCommand( + ClientId, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleLongResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture clientGetName() { + return commandManager.submitNewCommand( + ClientGetName, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture> clientGetName(@NonNull Route route) { + return commandManager.submitNewCommand( + ClientGetName, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleStringOrNullResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture configRewrite() { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configRewrite(@NonNull Route route) { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat() { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat(@NonNull Route route) { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture> configGet(@NonNull String[] parameters) { + return commandManager.submitNewCommand(ConfigGet, parameters, this::handleMapResponse); + } + + @Override + public CompletableFuture>> configGet( + @NonNull String[] parameters, @NonNull Route route) { + return commandManager.submitNewCommand( + ConfigGet, + parameters, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleMapResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture configSet(@NonNull Map parameters) { + return commandManager.submitNewCommand( + ConfigSet, convertMapToKeyValueStringArray(parameters), this::handleStringResponse); + } + + @Override + public CompletableFuture configSet( + @NonNull Map parameters, @NonNull Route route) { + return commandManager.submitNewCommand( + ConfigSet, convertMapToKeyValueStringArray(parameters), route, this::handleStringResponse); + } + + @Override + public CompletableFuture echo(@NonNull String message) { + return commandManager.submitNewCommand( + Echo, new String[] {message}, this::handleStringResponse); + } + + @Override + public CompletableFuture echo(@NonNull GlideString message) { + return commandManager.submitNewCommand( + Echo, new GlideString[] {message}, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture> echo( + @NonNull String message, @NonNull Route route) { + return commandManager.submitNewCommand( + Echo, + new String[] {message}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> echo( + @NonNull GlideString message, @NonNull Route route) { + return commandManager.submitNewCommand( + Echo, + new GlideString[] {message}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleGlideStringResponse(response)) + : ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture time() { + return commandManager.submitNewCommand( + Time, new String[0], response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture> time(@NonNull Route route) { + return commandManager.submitNewCommand( + Time, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(castArray(handleArrayResponse(response), String.class)) + : ClusterValue.ofMultiValue( + castMapOfArrays(handleMapResponse(response), String.class))); + } + + @Override + public CompletableFuture lastsave() { + return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture> lastsave(@NonNull Route route) { + return commandManager.submitNewCommand( + LastSave, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.of(handleLongResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture flushall() { + return commandManager.submitNewCommand(FlushAll, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture flushall(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FlushAll, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture flushall(@NonNull Route route) { + return commandManager.submitNewCommand( + FlushAll, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture flushall(@NonNull FlushMode mode, @NonNull Route route) { + return commandManager.submitNewCommand( + FlushAll, new String[] {mode.toString()}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture flushdb() { + return commandManager.submitNewCommand(FlushDB, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture flushdb(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FlushDB, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture flushdb(@NonNull Route route) { + return commandManager.submitNewCommand( + FlushDB, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture flushdb(@NonNull FlushMode mode, @NonNull Route route) { + return commandManager.submitNewCommand( + FlushDB, new String[] {mode.toString()}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut() { + return commandManager.submitNewCommand(Lolwut, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int @NonNull [] parameters) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + return commandManager.submitNewCommand(Lolwut, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version) { + return commandManager.submitNewCommand( + Lolwut, + new String[] {VERSION_VALKEY_API, Integer.toString(version)}, + this::handleStringResponse); + } + + @Override + public CompletableFuture lolwut(int version, int @NonNull [] parameters) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_VALKEY_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + return commandManager.submitNewCommand(Lolwut, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture> lolwut(@NonNull Route route) { + return commandManager.submitNewCommand( + Lolwut, + new String[0], + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> lolwut( + int @NonNull [] parameters, @NonNull Route route) { + String[] arguments = + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new); + return commandManager.submitNewCommand( + Lolwut, + arguments, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> lolwut(int version, @NonNull Route route) { + return commandManager.submitNewCommand( + Lolwut, + new String[] {VERSION_VALKEY_API, Integer.toString(version)}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> lolwut( + int version, int @NonNull [] parameters, @NonNull Route route) { + String[] arguments = + concatenateArrays( + new String[] {VERSION_VALKEY_API, Integer.toString(version)}, + Arrays.stream(parameters).mapToObj(Integer::toString).toArray(String[]::new)); + return commandManager.submitNewCommand( + Lolwut, + arguments, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleStringResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture dbsize() { + return commandManager.submitNewCommand(DBSize, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture dbsize(@NonNull Route route) { + return commandManager.submitNewCommand(DBSize, new String[0], route, this::handleLongResponse); + } + + @Override + public CompletableFuture functionLoad(@NonNull String libraryCode, boolean replace) { + String[] arguments = + replace ? new String[] {REPLACE.toString(), libraryCode} : new String[] {libraryCode}; + return commandManager.submitNewCommand(FunctionLoad, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture functionLoad( + @NonNull GlideString libraryCode, boolean replace) { + GlideString[] arguments = + replace + ? new GlideString[] {gs(REPLACE.toString()), libraryCode} + : new GlideString[] {libraryCode}; + return commandManager.submitNewCommand( + FunctionLoad, arguments, this::handleGlideStringResponse); + } + + @Override + public CompletableFuture functionLoad( + @NonNull String libraryCode, boolean replace, @NonNull Route route) { + String[] arguments = + replace ? new String[] {REPLACE.toString(), libraryCode} : new String[] {libraryCode}; + return commandManager.submitNewCommand( + FunctionLoad, arguments, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionLoad( + @NonNull GlideString libraryCode, boolean replace, @NonNull Route route) { + GlideString[] arguments = + replace + ? new GlideString[] {gs(REPLACE.toString()), libraryCode} + : new GlideString[] {libraryCode}; + return commandManager.submitNewCommand( + FunctionLoad, arguments, route, this::handleGlideStringResponse); + } + + /** Process a FUNCTION LIST cluster response. */ + protected ClusterValue[]> handleFunctionListResponse( + Response response, Route route) { + if (route instanceof SingleNodeRoute) { + Map[] data = handleFunctionListResponse(handleArrayResponse(response)); + return ClusterValue.ofSingleValue(data); + } else { + // each `Object` is a `Map[]` actually + Map info = handleMapResponse(response); + Map[]> data = new HashMap<>(); + for (var nodeInfo : info.entrySet()) { + data.put(nodeInfo.getKey(), handleFunctionListResponse((Object[]) nodeInfo.getValue())); + } + return ClusterValue.ofMultiValue(data); + } + } + + /** Process a FUNCTION LIST cluster response. */ + protected ClusterValue[]> handleFunctionListResponseBinary( + Response response, Route route) { + if (route instanceof SingleNodeRoute) { + Map[] data = + handleFunctionListResponseBinary(handleArrayResponseBinary(response)); + return ClusterValue.ofSingleValue(data); + } else { + // each `Object` is a `Map[]` actually + Map info = handleBinaryStringMapResponse(response); + Map[]> data = new HashMap<>(); + for (var nodeInfo : info.entrySet()) { + data.put( + nodeInfo.getKey(), handleFunctionListResponseBinary((Object[]) nodeInfo.getValue())); + } + return ClusterValue.ofMultiValueBinary(data); + } + } + + @Override + public CompletableFuture[]> functionList(boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode ? new String[] {WITH_CODE_VALKEY_API} : new String[0], + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]> functionListBinary(boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + new ArgsBuilder().addIf(WITH_CODE_VALKEY_API, withCode).toArray(), + response -> handleFunctionListResponseBinary(handleArrayResponseBinary(response))); + } + + @Override + public CompletableFuture[]> functionList( + @NonNull String libNamePattern, boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + withCode + ? new String[] {LIBRARY_NAME_VALKEY_API, libNamePattern, WITH_CODE_VALKEY_API} + : new String[] {LIBRARY_NAME_VALKEY_API, libNamePattern}, + response -> handleFunctionListResponse(handleArrayResponse(response))); + } + + @Override + public CompletableFuture[]> functionListBinary( + @NonNull GlideString libNamePattern, boolean withCode) { + return commandManager.submitNewCommand( + FunctionList, + new ArgsBuilder() + .add(LIBRARY_NAME_VALKEY_API) + .add(libNamePattern) + .addIf(WITH_CODE_VALKEY_API, withCode) + .toArray(), + response -> handleFunctionListResponseBinary(handleArrayResponseBinary(response))); + } + + @Override + public CompletableFuture[]>> functionList( + boolean withCode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionList, + withCode ? new String[] {WITH_CODE_VALKEY_API} : new String[0], + route, + response -> handleFunctionListResponse(response, route)); + } + + public CompletableFuture[]>> functionListBinary( + boolean withCode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionList, + new ArgsBuilder().addIf(WITH_CODE_VALKEY_API, withCode).toArray(), + route, + response -> handleFunctionListResponseBinary(response, route)); + } + + @Override + public CompletableFuture[]>> functionList( + @NonNull String libNamePattern, boolean withCode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionList, + withCode + ? new String[] {LIBRARY_NAME_VALKEY_API, libNamePattern, WITH_CODE_VALKEY_API} + : new String[] {LIBRARY_NAME_VALKEY_API, libNamePattern}, + route, + response -> handleFunctionListResponse(response, route)); + } + + public CompletableFuture[]>> functionListBinary( + @NonNull GlideString libNamePattern, boolean withCode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionList, + new ArgsBuilder() + .add(LIBRARY_NAME_VALKEY_API) + .add(libNamePattern) + .addIf(WITH_CODE_VALKEY_API, withCode) + .toArray(), + route, + response -> handleFunctionListResponseBinary(response, route)); + } + + @Override + public CompletableFuture functionFlush() { + return commandManager.submitNewCommand( + FunctionFlush, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FunctionFlush, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionFlush, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull FlushMode mode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionFlush, new String[] {mode.toString()}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete(@NonNull String libName) { + return commandManager.submitNewCommand( + FunctionDelete, new String[] {libName}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete(@NonNull GlideString libName) { + return commandManager.submitNewCommand( + FunctionDelete, new GlideString[] {libName}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete(@NonNull String libName, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionDelete, new String[] {libName}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete( + @NonNull GlideString libName, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionDelete, new GlideString[] {libName}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDump() { + return commandManager.submitNewCommand( + FunctionDump, new GlideString[] {}, this::handleBytesOrNullResponse); + } + + @Override + public CompletableFuture> functionDump(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionDump, + new GlideString[] {}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleBytesOrNullResponse(response)) + : ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + route, + this::handleStringResponse); + } + + @Override + public CompletableFuture fcall(@NonNull String function) { + return fcall(function, new String[0]); + } + + @Override + public CompletableFuture fcall(@NonNull GlideString function) { + return fcall(function, new GlideString[0]); + } + + @Override + public CompletableFuture> fcall( + @NonNull String function, @NonNull Route route) { + return fcall(function, new String[0], route); + } + + @Override + public CompletableFuture> fcall( + @NonNull GlideString function, @NonNull Route route) { + return fcall(function, new GlideString[0], route); + } + + @Override + public CompletableFuture fcall(@NonNull String function, @NonNull String[] arguments) { + String[] args = concatenateArrays(new String[] {function, "0"}, arguments); // 0 - key count + return commandManager.submitNewCommand(FCall, args, this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture fcall( + @NonNull GlideString function, @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand(FCall, args, this::handleBinaryObjectOrNullResponse); + } + + @Override + public CompletableFuture> fcall( + @NonNull String function, @NonNull String[] arguments, @NonNull Route route) { + String[] args = concatenateArrays(new String[] {function, "0"}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCall, + args, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> fcall( + @NonNull GlideString function, @NonNull GlideString[] arguments, @NonNull Route route) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCall, + args, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleBinaryObjectOrNullResponse(response)) + : ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture fcallReadOnly(@NonNull String function) { + return fcallReadOnly(function, new String[0]); + } + + @Override + public CompletableFuture fcallReadOnly(@NonNull GlideString function) { + return fcallReadOnly(function, new GlideString[0]); + } + + @Override + public CompletableFuture> fcallReadOnly( + @NonNull String function, @NonNull Route route) { + return fcallReadOnly(function, new String[0], route); + } + + @Override + public CompletableFuture> fcallReadOnly( + @NonNull GlideString function, @NonNull Route route) { + return fcallReadOnly(function, new GlideString[0], route); + } + + @Override + public CompletableFuture fcallReadOnly( + @NonNull String function, @NonNull String[] arguments) { + String[] args = concatenateArrays(new String[] {function, "0"}, arguments); // 0 - key count + return commandManager.submitNewCommand(FCallReadOnly, args, this::handleObjectOrNullResponse); + } + + @Override + public CompletableFuture fcallReadOnly( + @NonNull GlideString function, @NonNull GlideString[] arguments) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCallReadOnly, args, this::handleBinaryObjectOrNullResponse); + } + + @Override + public CompletableFuture> fcallReadOnly( + @NonNull String function, @NonNull String[] arguments, @NonNull Route route) { + String[] args = concatenateArrays(new String[] {function, "0"}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCallReadOnly, + args, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)) + : ClusterValue.ofMultiValue(handleMapResponse(response))); + } + + @Override + public CompletableFuture> fcallReadOnly( + @NonNull GlideString function, @NonNull GlideString[] arguments, @NonNull Route route) { + GlideString[] args = + concatenateArrays(new GlideString[] {function, gs("0")}, arguments); // 0 - key count + return commandManager.submitNewCommand( + FCallReadOnly, + args, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleBinaryObjectOrNullResponse(response)) + : ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture functionKill() { + return commandManager.submitNewCommand(FunctionKill, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture functionKill(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionKill, new String[0], route, this::handleStringResponse); + } + + /** Process a FUNCTION STATS cluster response. */ + protected ClusterValue>> handleFunctionStatsResponse( + Response response, boolean isSingleValue) { + if (isSingleValue) { + return ClusterValue.ofSingleValue(handleFunctionStatsResponse(handleMapResponse(response))); + } else { + Map>> data = handleMapResponse(response); + for (var nodeInfo : data.entrySet()) { + nodeInfo.setValue(handleFunctionStatsResponse(nodeInfo.getValue())); + } + return ClusterValue.ofMultiValue(data); + } + } + + /** Process a FUNCTION STATS cluster response. */ + protected ClusterValue>> + handleFunctionStatsBinaryResponse(Response response, boolean isSingleValue) { + if (isSingleValue) { + return ClusterValue.ofSingleValue( + handleFunctionStatsBinaryResponse(handleBinaryStringMapResponse(response))); + } else { + Map>> data = + handleBinaryStringMapResponse(response); + for (var nodeInfo : data.entrySet()) { + nodeInfo.setValue(handleFunctionStatsBinaryResponse(nodeInfo.getValue())); + } + return ClusterValue.ofMultiValueBinary(data); + } + } + + @Override + public CompletableFuture>>> functionStats() { + return commandManager.submitNewCommand( + FunctionStats, new String[0], response -> handleFunctionStatsResponse(response, false)); + } + + @Override + public CompletableFuture>>> + functionStatsBinary() { + return commandManager.submitNewCommand( + FunctionStats, + new GlideString[0], + response -> handleFunctionStatsBinaryResponse(response, false)); + } + + @Override + public CompletableFuture>>> functionStats( + @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionStats, + new String[0], + route, + response -> handleFunctionStatsResponse(response, route instanceof SingleNodeRoute)); + } + + @Override + public CompletableFuture>>> + functionStatsBinary(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionStats, + new GlideString[0], + route, + response -> handleFunctionStatsBinaryResponse(response, route instanceof SingleNodeRoute)); + } + + public CompletableFuture publish( + @NonNull String message, @NonNull String channel, boolean sharded) { + if (!sharded) { + return publish(message, channel); + } + + return commandManager.submitNewCommand( + SPublish, + new String[] {channel, message}, + response -> { + // Check, but ignore the number - it is never valid. A GLIDE bug/limitation TODO + handleLongResponse(response); + return OK; + }); + } + + @Override + public CompletableFuture publish( + @NonNull GlideString message, @NonNull GlideString channel, boolean sharded) { + if (!sharded) { + return publish(message, channel); + } + + return commandManager.submitNewCommand( + SPublish, + new GlideString[] {channel, message}, + response -> { + // Check, but ignore the number - it is never valid. A GLIDE bug/limitation TODO + handleLongResponse(response); + return OK; + }); + } + + @Override + public CompletableFuture unwatch(@NonNull Route route) { + return commandManager.submitNewCommand( + UnWatch, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture unwatch() { + return commandManager.submitNewCommand(UnWatch, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture randomKey(@NonNull Route route) { + return commandManager.submitNewCommand( + RandomKey, new String[0], route, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture randomKeyBinary(@NonNull Route route) { + return commandManager.submitNewCommand( + RandomKey, new GlideString[0], route, this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture randomKey() { + return commandManager.submitNewCommand( + RandomKey, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture randomKeyBinary() { + return commandManager.submitNewCommand( + RandomKey, new GlideString[0], this::handleGlideStringOrNullResponse); + } + + @Override + public CompletableFuture scan(ClusterScanCursor cursor) { + return commandManager + .submitClusterScan(cursor, ScanOptions.builder().build(), this::handleArrayResponse) + .thenApply( + result -> new Object[] {new NativeClusterScanCursor(result[0].toString()), result[1]}); + } + + @Override + public CompletableFuture scanBinary(ClusterScanCursor cursor) { + return commandManager + .submitClusterScan(cursor, ScanOptions.builder().build(), this::handleArrayResponseBinary) + .thenApply( + result -> new Object[] {new NativeClusterScanCursor(result[0].toString()), result[1]}); + } + + @Override + public CompletableFuture scan(ClusterScanCursor cursor, ScanOptions options) { + return commandManager + .submitClusterScan(cursor, options, this::handleArrayResponse) + .thenApply( + result -> new Object[] {new NativeClusterScanCursor(result[0].toString()), result[1]}); + } + + @Override + public CompletableFuture scanBinary(ClusterScanCursor cursor, ScanOptions options) { + return commandManager + .submitClusterScan(cursor, options, this::handleArrayResponseBinary) + .thenApply( + result -> new Object[] {new NativeClusterScanCursor(result[0].toString()), result[1]}); + } + + @Override + public CompletableFuture sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sort( + @NonNull GlideString key, @NonNull SortClusterOptions sortClusterOptions) { + GlideString[] arguments = new ArgsBuilder().add(key).add(sortClusterOptions.toArgs()).toArray(); + + return commandManager.submitNewCommand( + Sort, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull GlideString key, @NonNull SortClusterOptions sortClusterOptions) { + GlideString[] arguments = new ArgsBuilder().add(key).add(sortClusterOptions.toArgs()).toArray(); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayOrNullResponseBinary(response), GlideString.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } + + @Override + public CompletableFuture sortStore( + @NonNull GlideString key, + @NonNull GlideString destination, + @NonNull SortClusterOptions sortClusterOptions) { + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(sortClusterOptions.toArgs()) + .add(STORE_COMMAND_STRING) + .add(destination) + .toArray(); + + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } + + /** A {@link ClusterScanCursor} implementation for interacting with the Rust layer. */ + private static final class NativeClusterScanCursor + implements CommandManager.ClusterScanCursorDetail { + + private final String cursorHandle; + private final boolean isFinished; + private boolean isClosed = false; + + // This is for internal use only. + public NativeClusterScanCursor(@NonNull String cursorHandle) { + this.cursorHandle = cursorHandle; + this.isFinished = ClusterScanCursorResolver.FINISHED_CURSOR_HANDLE.equals(cursorHandle); + } + + @Override + public String getCursorHandle() { + return cursorHandle; + } + + @Override + public boolean isFinished() { + return isFinished; + } + + @Override + public void releaseCursorHandle() { + internalClose(); + } + + @Override + protected void finalize() throws Throwable { + try { + // Release the native cursor + this.internalClose(); + } finally { + super.finalize(); + } + } + + private void internalClose() { + if (!isClosed) { + try { + ClusterScanCursorResolver.releaseNativeCursor(cursorHandle); + } catch (Exception ex) { + Logger.log( + Logger.Level.ERROR, + "ClusterScanCursor", + () -> "Error releasing cursor " + cursorHandle, + ex); + } finally { + // Mark the cursor as closed to avoid double-free (if close() gets called more than once). + isClosed = true; + } + } + } + } +} diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java deleted file mode 100644 index 2d1aeda088..0000000000 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ /dev/null @@ -1,141 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api; - -import static glide.utils.ArrayTransformUtils.castArray; -import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; -import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; -import static redis_request.RedisRequestOuterClass.RequestType.ClientId; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigGet; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; -import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; -import static redis_request.RedisRequestOuterClass.RequestType.Echo; -import static redis_request.RedisRequestOuterClass.RequestType.Info; -import static redis_request.RedisRequestOuterClass.RequestType.LastSave; -import static redis_request.RedisRequestOuterClass.RequestType.Ping; -import static redis_request.RedisRequestOuterClass.RequestType.Select; -import static redis_request.RedisRequestOuterClass.RequestType.Time; - -import glide.api.commands.ConnectionManagementCommands; -import glide.api.commands.GenericCommands; -import glide.api.commands.ServerManagementCommands; -import glide.api.models.Transaction; -import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.RedisClientConfiguration; -import glide.managers.CommandManager; -import glide.managers.ConnectionManager; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import lombok.NonNull; - -/** - * Async (non-blocking) client for Redis in Standalone mode. Use {@link #CreateClient} to request a - * client to Redis. - */ -public class RedisClient extends BaseClient - implements GenericCommands, ServerManagementCommands, ConnectionManagementCommands { - - protected RedisClient(ConnectionManager connectionManager, CommandManager commandManager) { - super(connectionManager, commandManager); - } - - /** - * Async request for an async (non-blocking) Redis client in Standalone mode. - * - * @param config Redis client Configuration - * @return A Future to connect and return a RedisClient - */ - public static CompletableFuture CreateClient( - @NonNull RedisClientConfiguration config) { - return CreateClient(config, RedisClient::new); - } - - @Override - public CompletableFuture customCommand(@NonNull String[] args) { - return commandManager.submitNewCommand(CustomCommand, args, this::handleObjectOrNullResponse); - } - - @Override - public CompletableFuture exec(@NonNull Transaction transaction) { - return commandManager.submitNewCommand(transaction, this::handleArrayOrNullResponse); - } - - @Override - public CompletableFuture ping() { - return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture ping(@NonNull String message) { - return commandManager.submitNewCommand( - Ping, new String[] {message}, this::handleStringResponse); - } - - @Override - public CompletableFuture info() { - return commandManager.submitNewCommand(Info, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture info(@NonNull InfoOptions options) { - return commandManager.submitNewCommand(Info, options.toArgs(), this::handleStringResponse); - } - - @Override - public CompletableFuture select(long index) { - return commandManager.submitNewCommand( - Select, new String[] {Long.toString(index)}, this::handleStringResponse); - } - - @Override - public CompletableFuture clientId() { - return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); - } - - @Override - public CompletableFuture clientGetName() { - return commandManager.submitNewCommand( - ClientGetName, new String[0], this::handleStringOrNullResponse); - } - - @Override - public CompletableFuture configRewrite() { - return commandManager.submitNewCommand( - ConfigRewrite, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture configResetStat() { - return commandManager.submitNewCommand( - ConfigResetStat, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture> configGet(@NonNull String[] parameters) { - return commandManager.submitNewCommand(ConfigGet, parameters, this::handleMapResponse); - } - - @Override - public CompletableFuture configSet(@NonNull Map parameters) { - return commandManager.submitNewCommand( - ConfigSet, convertMapToKeyValueStringArray(parameters), this::handleStringResponse); - } - - @Override - public CompletableFuture echo(@NonNull String message) { - return commandManager.submitNewCommand( - Echo, new String[] {message}, this::handleStringResponse); - } - - @Override - public CompletableFuture time() { - return commandManager.submitNewCommand( - Time, new String[0], response -> castArray(handleArrayResponse(response), String.class)); - } - - @Override - public CompletableFuture lastsave() { - return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); - } -} diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java deleted file mode 100644 index c2aec33d34..0000000000 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ /dev/null @@ -1,300 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api; - -import static glide.utils.ArrayTransformUtils.castArray; -import static glide.utils.ArrayTransformUtils.castMapOfArrays; -import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; -import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; -import static redis_request.RedisRequestOuterClass.RequestType.ClientId; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigGet; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; -import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; -import static redis_request.RedisRequestOuterClass.RequestType.Echo; -import static redis_request.RedisRequestOuterClass.RequestType.Info; -import static redis_request.RedisRequestOuterClass.RequestType.LastSave; -import static redis_request.RedisRequestOuterClass.RequestType.Ping; -import static redis_request.RedisRequestOuterClass.RequestType.Time; - -import glide.api.commands.ConnectionManagementClusterCommands; -import glide.api.commands.GenericClusterCommands; -import glide.api.commands.ServerManagementClusterCommands; -import glide.api.models.ClusterTransaction; -import glide.api.models.ClusterValue; -import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.RedisClusterClientConfiguration; -import glide.api.models.configuration.RequestRoutingConfiguration.Route; -import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; -import glide.managers.CommandManager; -import glide.managers.ConnectionManager; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import lombok.NonNull; -import response.ResponseOuterClass.Response; - -/** - * Async (non-blocking) client for Redis in Cluster mode. Use {@link #CreateClient} to request a - * client to Redis. - */ -public class RedisClusterClient extends BaseClient - implements ConnectionManagementClusterCommands, - GenericClusterCommands, - ServerManagementClusterCommands { - - protected RedisClusterClient(ConnectionManager connectionManager, CommandManager commandManager) { - super(connectionManager, commandManager); - } - - /** - * Async request for an async (non-blocking) Redis client in Cluster mode. - * - * @param config Redis cluster client Configuration - * @return A Future to connect and return a RedisClusterClient - */ - public static CompletableFuture CreateClient( - @NonNull RedisClusterClientConfiguration config) { - return CreateClient(config, RedisClusterClient::new); - } - - @Override - public CompletableFuture> customCommand(@NonNull String[] args) { - // TODO if a command returns a map as a single value, ClusterValue misleads user - return commandManager.submitNewCommand( - CustomCommand, args, response -> ClusterValue.of(handleObjectOrNullResponse(response))); - } - - @Override - public CompletableFuture> customCommand( - @NonNull String[] args, @NonNull Route route) { - return commandManager.submitNewCommand( - CustomCommand, args, route, response -> handleCustomCommandResponse(route, response)); - } - - protected ClusterValue handleCustomCommandResponse(Route route, Response response) { - if (route instanceof SingleNodeRoute) { - return ClusterValue.ofSingleValue(handleObjectOrNullResponse(response)); - } - if (response.hasConstantResponse()) { - return ClusterValue.ofSingleValue(handleStringResponse(response)); - } - return ClusterValue.ofMultiValue(handleMapResponse(response)); - } - - @Override - public CompletableFuture exec(@NonNull ClusterTransaction transaction) { - return commandManager.submitNewCommand( - transaction, Optional.empty(), this::handleArrayOrNullResponse); - } - - @Override - public CompletableFuture exec( - @NonNull ClusterTransaction transaction, @NonNull SingleNodeRoute route) { - return commandManager.submitNewCommand( - transaction, Optional.of(route), this::handleArrayOrNullResponse); - } - - @Override - public CompletableFuture ping() { - return commandManager.submitNewCommand(Ping, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture ping(@NonNull String message) { - return commandManager.submitNewCommand( - Ping, new String[] {message}, this::handleStringResponse); - } - - @Override - public CompletableFuture ping(@NonNull Route route) { - return commandManager.submitNewCommand(Ping, new String[0], route, this::handleStringResponse); - } - - @Override - public CompletableFuture ping(@NonNull String message, @NonNull Route route) { - return commandManager.submitNewCommand( - Ping, new String[] {message}, route, this::handleStringResponse); - } - - @Override - public CompletableFuture> info() { - return commandManager.submitNewCommand( - Info, new String[0], response -> ClusterValue.of(handleMapResponse(response))); - } - - public CompletableFuture> info(@NonNull Route route) { - return commandManager.submitNewCommand( - Info, - new String[0], - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.of(handleStringResponse(response)) - : ClusterValue.of(handleMapResponse(response))); - } - - @Override - public CompletableFuture> info(@NonNull InfoOptions options) { - return commandManager.submitNewCommand( - Info, options.toArgs(), response -> ClusterValue.of(handleMapResponse(response))); - } - - @Override - public CompletableFuture> info( - @NonNull InfoOptions options, @NonNull Route route) { - return commandManager.submitNewCommand( - Info, - options.toArgs(), - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.of(handleStringResponse(response)) - : ClusterValue.of(handleMapResponse(response))); - } - - @Override - public CompletableFuture clientId() { - return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); - } - - @Override - public CompletableFuture> clientId(@NonNull Route route) { - return commandManager.submitNewCommand( - ClientId, - new String[0], - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.of(handleLongResponse(response)) - : ClusterValue.of(handleMapResponse(response))); - } - - @Override - public CompletableFuture clientGetName() { - return commandManager.submitNewCommand( - ClientGetName, new String[0], this::handleStringOrNullResponse); - } - - @Override - public CompletableFuture> clientGetName(@NonNull Route route) { - return commandManager.submitNewCommand( - ClientGetName, - new String[0], - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.of(handleStringOrNullResponse(response)) - : ClusterValue.of(handleMapResponse(response))); - } - - @Override - public CompletableFuture configRewrite() { - return commandManager.submitNewCommand( - ConfigRewrite, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture configRewrite(@NonNull Route route) { - return commandManager.submitNewCommand( - ConfigRewrite, new String[0], route, this::handleStringResponse); - } - - @Override - public CompletableFuture configResetStat() { - return commandManager.submitNewCommand( - ConfigResetStat, new String[0], this::handleStringResponse); - } - - @Override - public CompletableFuture configResetStat(@NonNull Route route) { - return commandManager.submitNewCommand( - ConfigResetStat, new String[0], route, this::handleStringResponse); - } - - @Override - public CompletableFuture> configGet(@NonNull String[] parameters) { - return commandManager.submitNewCommand(ConfigGet, parameters, this::handleMapResponse); - } - - @Override - public CompletableFuture>> configGet( - @NonNull String[] parameters, @NonNull Route route) { - return commandManager.submitNewCommand( - ConfigGet, - parameters, - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.ofSingleValue(handleMapResponse(response)) - : ClusterValue.ofMultiValue(handleMapResponse(response))); - } - - @Override - public CompletableFuture configSet(@NonNull Map parameters) { - return commandManager.submitNewCommand( - ConfigSet, convertMapToKeyValueStringArray(parameters), this::handleStringResponse); - } - - @Override - public CompletableFuture configSet( - @NonNull Map parameters, @NonNull Route route) { - return commandManager.submitNewCommand( - ConfigSet, convertMapToKeyValueStringArray(parameters), route, this::handleStringResponse); - } - - @Override - public CompletableFuture echo(@NonNull String message) { - return commandManager.submitNewCommand( - Echo, new String[] {message}, this::handleStringResponse); - } - - @Override - public CompletableFuture> echo( - @NonNull String message, @NonNull Route route) { - return commandManager.submitNewCommand( - Echo, - new String[] {message}, - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.ofSingleValue(handleStringResponse(response)) - : ClusterValue.ofMultiValue(handleMapResponse(response))); - } - - @Override - public CompletableFuture time() { - return commandManager.submitNewCommand( - Time, new String[0], response -> castArray(handleArrayResponse(response), String.class)); - } - - @Override - public CompletableFuture> time(@NonNull Route route) { - return commandManager.submitNewCommand( - Time, - new String[0], - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.ofSingleValue(castArray(handleArrayResponse(response), String.class)) - : ClusterValue.ofMultiValue( - castMapOfArrays(handleMapResponse(response), String.class))); - } - - @Override - public CompletableFuture lastsave() { - return commandManager.submitNewCommand(LastSave, new String[0], this::handleLongResponse); - } - - @Override - public CompletableFuture> lastsave(@NonNull Route route) { - return commandManager.submitNewCommand( - LastSave, - new String[0], - route, - response -> - route instanceof SingleNodeRoute - ? ClusterValue.of(handleLongResponse(response)) - : ClusterValue.of(handleMapResponse(response))); - } -} diff --git a/java/client/src/main/java/glide/api/ResponseFlags.java b/java/client/src/main/java/glide/api/ResponseFlags.java new file mode 100644 index 0000000000..84a5c666df --- /dev/null +++ b/java/client/src/main/java/glide/api/ResponseFlags.java @@ -0,0 +1,9 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api; + +public enum ResponseFlags { + /** Strings in the response are UTF-8 encoded */ + ENCODING_UTF8, + /** Null is a valid response */ + IS_NULLABLE, +} diff --git a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java new file mode 100644 index 0000000000..22612c9bd5 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -0,0 +1,610 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; + +import glide.api.models.GlideString; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.configuration.ReadFrom; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands and transactions for the "Bitmap Commands" group for standalone and cluster + * clients. + * + * @see Bitmap Commands + */ +public interface BitmapBaseCommands { + /** + * Counts the number of set bits (population counting) in a string stored at key. + * + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @return The number of set bits in the string. Returns zero if the key is missing as it is + * treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount("myKey1").get();
+     * assert payload == 2L; // The string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(String key); + + /** + * Counts the number of set bits (population counting) in a string stored at key. + * + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @return The number of set bits in the string. Returns zero if the key is missing as it is + * treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1")).get();
+     * assert payload == 2L; // The string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(GlideString key); + + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset byte index. + * @param end The ending offset byte index. + * @return The number of set bits in the string byte interval specified by start and + * end. Returns zero if the key is missing as it is treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount("myKey1", 1, 3).get();
+     * assert payload == 2L; // The second to fourth bytes of the string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(String key, long start, long end); + + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset byte index. + * @param end The ending offset byte index. + * @return The number of set bits in the string byte interval specified by start and + * end. Returns zero if the key is missing as it is treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1"), 1, 3).get();
+     * assert payload == 2L; // The second to fourth bytes of the string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(GlideString key, long start, long end); + + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @since Valkey 7.0 and above + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset. + * @param end The ending offset. + * @param options The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The number of set bits in the string interval specified by start, + * end, and options. Returns zero if the key is missing as it is treated + * as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount("myKey1", 1, 1, BIT).get();
+     * assert payload == 1L; // Indicates that the second bit of the string stored at "myKey1" is set.
+     * }
+ */ + CompletableFuture bitcount(String key, long start, long end, BitmapIndexType options); + + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @since Valkey 7.0 and above + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset. + * @param end The ending offset. + * @param options The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The number of set bits in the string interval specified by start, + * end, and options. Returns zero if the key is missing as it is treated + * as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1"), 1, 1, BIT).get();
+     * assert payload == 1L; // Indicates that the second bit of the string stored at "myKey1" is set.
+     * }
+ */ + CompletableFuture bitcount(GlideString key, long start, long end, BitmapIndexType options); + + /** + * Sets or clears the bit at offset in the string value stored at key. + * The offset is a zero-based index, with 0 being the first element of + * the list, 1 being the next element, and so on. The offset must be + * less than 2^32 and greater than or equal to 0. If a key is + * non-existent then the bit at offset is set to value and the preceding + * bits are set to 0. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param offset The index of the bit to be set. + * @param value The bit value to set at offset. The value must be 0 or + * 1. + * @return The bit value that was previously stored at offset. + * @example + *
{@code
+     * Long payload = client.setbit("myKey1", 1, 1).get();
+     * assert payload == 0L; // The second bit value was 0 before setting to 1.
+     * }
+ */ + CompletableFuture setbit(String key, long offset, long value); + + /** + * Sets or clears the bit at offset in the string value stored at key. + * The offset is a zero-based index, with 0 being the first element of + * the list, 1 being the next element, and so on. The offset must be + * less than 2^32 and greater than or equal to 0. If a key is + * non-existent then the bit at offset is set to value and the preceding + * bits are set to 0. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param offset The index of the bit to be set. + * @param value The bit value to set at offset. The value must be 0 or + * 1. + * @return The bit value that was previously stored at offset. + * @example + *
{@code
+     * Long payload = client.setbit(gs("myKey1"), 1, 1).get();
+     * assert payload == 0L; // The second bit value was 0 before setting to 1.
+     * }
+ */ + CompletableFuture setbit(GlideString key, long offset, long value); + + /** + * Returns the bit value at offset in the string value stored at key. + * offset should be greater than or equal to zero. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param offset The index of the bit to return. + * @return The bit at offset of the string. Returns zero if the key is empty or if the positive + * offset exceeds the length of the string. + * @example + *
{@code
+     * client.set("sampleKey", "A"); // "A" has binary value 01000001
+     * Long payload = client.getbit("sampleKey", 1).get();
+     * assert payload == 1L; // The second bit for string stored at "sampleKey" is set to 1.
+     * }
+ */ + CompletableFuture getbit(String key, long offset); + + /** + * Returns the bit value at offset in the string value stored at key. + * offset should be greater than or equal to zero. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param offset The index of the bit to return. + * @return The bit at offset of the string. Returns zero if the key is empty or if the positive + * offset exceeds the length of the string. + * @example + *
{@code
+     * client.set(gs("sampleKey"), gs("A")); // "A" has binary value 01000001
+     * Long payload = client.getbit(gs("sampleKey"), 1).get();
+     * assert payload == 1L; // The second bit for string stored at "sampleKey" is set to 1.
+     * }
+ */ + CompletableFuture getbit(GlideString key, long offset); + + /** + * Returns the position of the first bit matching the given bit value. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @return The position of the first occurrence matching bit in the binary value of + * the string held at key. If bit is not found, a -1 is + * returned. + * @example + *
{@code
+     * Long payload = client.bitpos("myKey1", 0).get();
+     * // Indicates that the first occurrence of a 0 bit value is the fourth bit of the binary value
+     * // of the string stored at "myKey1".
+     * assert payload == 3L;
+     * }
+ */ + CompletableFuture bitpos(String key, long bit); + + /** + * Returns the position of the first bit matching the given bit value. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @return The position of the first occurrence matching bit in the binary value of + * the string held at key. If bit is not found, a -1 is + * returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 0).get();
+     * // Indicates that the first occurrence of a 0 bit value is the fourth bit of the binary value
+     * // of the string stored at "myKey1".
+     * assert payload == 3L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit); + + /** + * Returns the position of the first bit matching the given bit value. The offset + * start is a zero-based index, with 0 being the first byte of the list, + * 1 being the next byte and so on. These offsets can also be negative numbers + * indicating offsets starting at the end of the list, with -1 being the last byte of + * the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @return The position of the first occurrence beginning at the start offset of the + * bit in the binary value of the string held at key. If bit + * is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos("myKey1", 1, 4).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from fifth byte is the 34th
+     * // bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(String key, long bit, long start); + + /** + * Returns the position of the first bit matching the given bit value. The offset + * start is a zero-based index, with 0 being the first byte of the list, + * 1 being the next byte and so on. These offsets can also be negative numbers + * indicating offsets starting at the end of the list, with -1 being the last byte of + * the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @return The position of the first occurrence beginning at the start offset of the + * bit in the binary value of the string held at key. If bit + * is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from fifth byte is the 34th
+     * // bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit, long start); + + /** + * Returns the position of the first bit matching the given bit value. The offsets + * start and end are zero-based indexes, with 0 being the + * first byte of the list, 1 being the next byte and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being + * the last byte of the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos("myKey1", 1, 4, 6).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bytes is the 34th bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(String key, long bit, long start, long end); + + /** + * Returns the position of the first bit matching the given bit value. The offsets + * start and end are zero-based indexes, with 0 being the + * first byte of the list, 1 being the next byte and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being + * the last byte of the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4, 6).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bytes is the 34th bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit, long start, long end); + + /** + * Returns the position of the first bit matching the given bit value. The offset + * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, + * start==0 and end==2 means to look at the first three bits. If BYTE is + * specified, start==0 and end==2 means to look at the first three bytes + * The offsets are zero-based indexes, with 0 being the first element of the list, + * 1 being the next, and so on. These offsets can also be negative numbers indicating + * offsets starting at the end of the list, with -1 being the last element of the + * list, -2 being the penultimate, and so on. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @param offsetType The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos("myKey1", 1, 4, 6, BIT).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bits is the sixth bit of the binary value of the string stored at "myKey1".
+     * assert payload == 5L;
+     * }
+ */ + CompletableFuture bitpos( + String key, long bit, long start, long end, BitmapIndexType offsetType); + + /** + * Returns the position of the first bit matching the given bit value. The offset + * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, + * start==0 and end==2 means to look at the first three bits. If BYTE is + * specified, start==0 and end==2 means to look at the first three bytes + * The offsets are zero-based indexes, with 0 being the first element of the list, + * 1 being the next, and so on. These offsets can also be negative numbers indicating + * offsets starting at the end of the list, with -1 being the last element of the + * list, -2 being the penultimate, and so on. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @param offsetType The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4, 6, BIT).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bits is the sixth bit of the binary value of the string stored at "myKey1".
+     * assert payload == 5L;
+     * }
+ */ + CompletableFuture bitpos( + GlideString key, long bit, long start, long end, BitmapIndexType offsetType); + + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the + * result in the destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. + * @param bitwiseOperation The bitwise operation to perform. + * @param destination The key that will store the resulting string. + * @param keys The list of keys to perform the bitwise operation on. + * @return The size of the string stored in destination. + * @example + *
{@code
+     * client.set("key1", "A"); // "A" has binary value 01000001
+     * client.set("key2", "B"); // "B" has binary value 01000010
+     * Long payload = client.bitop(BitwiseOperation.AND, "destination", new String[] {key1, key2}).get();
+     * assert "@".equals(client.get("destination").get()); // "@" has binary value 01000000
+     * assert payload == 1L; // The size of the resulting string is 1.
+     * }
+ */ + CompletableFuture bitop( + BitwiseOperation bitwiseOperation, String destination, String[] keys); + + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the + * result in the destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. + * @param bitwiseOperation The bitwise operation to perform. + * @param destination The key that will store the resulting string. + * @param keys The list of keys to perform the bitwise operation on. + * @return The size of the string stored in destination. + * @example + *
{@code
+     * client.set("key1", "A"); // "A" has binary value 01000001
+     * client.set("key2", "B"); // "B" has binary value 01000010
+     * Long payload = client.bitop(BitwiseOperation.AND, gs("destination"), new GlideString[] {key1, key2}).get();
+     * assert "@".equals(client.get("destination").get()); // "@" has binary value 01000000
+     * assert payload == 1L; // The size of the resulting string is 1.
+     * }
+ */ + CompletableFuture bitop( + BitwiseOperation bitwiseOperation, GlideString destination, GlideString[] keys); + + /** + * Reads or modifies the array of bits representing the string that is held at key + * based on the specified subCommands. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param subCommands The subCommands to be performed on the binary value of the string at + * key, which could be any of the following: + *
    + *
  • {@link BitFieldGet}. + *
  • {@link BitFieldSet}. + *
  • {@link BitFieldIncrby}. + *
  • {@link BitFieldOverflow}. + *
+ * + * @return An array of results from the executed subcommands. + *
    + *
  • {@link BitFieldGet} returns the value in {@link Offset} or {@link OffsetMultiplier}. + *
  • {@link BitFieldSet} returns the old value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldIncrby} returns the new value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldOverflow} determines the behaviour of SET and + * INCRBY when an overflow occurs. OVERFLOW does not return a value + * and does not contribute a value to the array response. + *
+ * + * @example + *
{@code
+     * client.set("sampleKey", "A"); // "A" has binary value 01000001
+     * BitFieldSubCommands[] subcommands = new BitFieldSubCommands[] {
+     *      new BitFieldSet(new UnsignedEncoding(2), new Offset(1), 3), // Sets the new binary value to 01100001
+     *      new BitFieldGet(new UnsignedEncoding(2), new Offset(1)) // Gets value from 0(11)00001
+     * };
+     * Long[] payload = client.bitfield("sampleKey", subcommands).get();
+     * assertArrayEquals(payload, new Long[] {2L, 3L});
+     * }
+ */ + CompletableFuture bitfield(String key, BitFieldSubCommands[] subCommands); + + /** + * Reads or modifies the array of bits representing the string that is held at key + * based on the specified subCommands. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param subCommands The subCommands to be performed on the binary value of the string at + * key, which could be any of the following: + *
    + *
  • {@link BitFieldGet}. + *
  • {@link BitFieldSet}. + *
  • {@link BitFieldIncrby}. + *
  • {@link BitFieldOverflow}. + *
+ * + * @return An array of results from the executed subcommands. + *
    + *
  • {@link BitFieldGet} returns the value in {@link Offset} or {@link OffsetMultiplier}. + *
  • {@link BitFieldSet} returns the old value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldIncrby} returns the new value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldOverflow} determines the behaviour of SET and + * INCRBY when an overflow occurs. OVERFLOW does not return a value + * and does not contribute a value to the array response. + *
+ * + * @example + *
{@code
+     * client.set(gs("sampleKey"), gs("A")); // string "A" has binary value 01000001
+     * BitFieldSubCommands[] subcommands = new BitFieldSubCommands[] {
+     *      new BitFieldSet(new UnsignedEncoding(2), new Offset(1), 3), // Sets the new binary value to 01100001
+     *      new BitFieldGet(new UnsignedEncoding(2), new Offset(1)) // Gets value from 0(11)00001
+     * };
+     * Long[] payload = client.bitfield(gs("sampleKey"), subcommands).get();
+     * assertArrayEquals(payload, new Long[] {2L, 3L});
+     * }
+ */ + CompletableFuture bitfield(GlideString key, BitFieldSubCommands[] subCommands); + + /** + * Reads the array of bits representing the string that is held at key based on the + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 6.0 and above + * @see valkey.io for details. + * @param key The key of the string. + * @param subCommands The GET subCommands to be performed. + * @return An array of results from the GET subcommands. + * @example + *
{@code
+     * client.set("sampleKey", "A"); // "A" has binary value 01000001
+     * Long[] payload =
+     *      client.
+     *          bitfieldReadOnly(
+     *              "sampleKey",
+     *              new BitFieldReadOnlySubCommands[] {
+     *                  new BitFieldGet(new UnsignedEncoding(2), new Offset(1))
+     *              })
+     *          .get();
+     * assertArrayEquals(payload, new Long[] {2L}); // Value is from 0(10)00001
+     * }
+ */ + CompletableFuture bitfieldReadOnly(String key, BitFieldReadOnlySubCommands[] subCommands); + + /** + * Reads the array of bits representing the string that is held at key based on the + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 6.0 and above + * @see valkey.io for details. + * @param key The key of the string. + * @param subCommands The GET subCommands to be performed. + * @return An array of results from the GET subcommands. + * @example + *
{@code
+     * client.set(gs("sampleKey"), gs("A")); //string "A" has binary value 01000001
+     * Long[] payload =
+     *      client.
+     *          bitfieldReadOnly(
+     *              gs("sampleKey"),
+     *              new BitFieldReadOnlySubCommands[] {
+     *                  new BitFieldGet(new UnsignedEncoding(2), new Offset(1))
+     *              })
+     *          .get();
+     * assertArrayEquals(payload, new Long[] {2L}); // Value is from 0(10)00001
+     * }
+ */ + CompletableFuture bitfieldReadOnly( + GlideString key, BitFieldReadOnlySubCommands[] subCommands); +} diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java index 1ce9e8a511..9c1293393b 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java @@ -1,22 +1,23 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.concurrent.CompletableFuture; /** * Supports commands for the "Connection Management" group for a cluster client. * - * @see Connection Management Commands + * @see Connection Management Commands */ public interface ConnectionManagementClusterCommands { /** - * Pings the Redis server.
+ * Pings the server.
* The command will be routed to all primary nodes. * - * @see redis.io for details. + * @see valkey.io for details. * @return String with "PONG". * @example *
{@code
@@ -27,10 +28,10 @@ public interface ConnectionManagementClusterCommands {
     CompletableFuture ping();
 
     /**
-     * Pings the Redis server.
+ * Pings the server.
* The command will be routed to all primary nodes. * - * @see redis.io for details. + * @see valkey.io for details. * @param message The server will respond with a copy of the message. * @return String with a copy of the argument message. * @example @@ -42,9 +43,24 @@ public interface ConnectionManagementClusterCommands { CompletableFuture ping(String message); /** - * Pings the Redis server. + * Pings the server.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @param message The server will respond with a copy of the message. + * @return GlideString with a copy of the argument message. + * @example + *
{@code
+     * GlideString payload = clusterClient.ping(gs("GLIDE")).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture ping(GlideString message); + + /** + * Pings the server. * - * @see redis.io for details. + * @see valkey.io for details. * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. * @return String with "PONG". @@ -57,9 +73,9 @@ public interface ConnectionManagementClusterCommands { CompletableFuture ping(Route route); /** - * Pings the Redis server. + * Pings the server. * - * @see redis.io for details. + * @see valkey.io for details. * @param message The ping argument that will be returned. * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. @@ -72,11 +88,27 @@ public interface ConnectionManagementClusterCommands { */ CompletableFuture ping(String message, Route route); + /** + * Pings the server. + * + * @see valkey.io for details. + * @param message The ping argument that will be returned. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return GlideString with a copy of the argument message. + * @example + *
{@code
+     * GlideString payload = clusterClient.ping(gs("GLIDE"), RANDOM).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture ping(GlideString message, Route route); + /** * Gets the current connection id.
* The command will be routed to a random node. * - * @see redis.io for details. + * @see valkey.io for details. * @return The id of the client. * @example *
{@code
@@ -89,7 +121,7 @@ public interface ConnectionManagementClusterCommands {
     /**
      * Gets the current connection id.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param route Specifies the routing configuration for the command. The client will route the
      *     command to the nodes defined by route.
      * @return A {@link ClusterValue} which holds a single value if single node route is used or a
@@ -110,7 +142,7 @@ public interface ConnectionManagementClusterCommands {
      * Gets the name of the current connection.
* The command will be routed a random node. * - * @see redis.io for details. + * @see valkey.io for details. * @return The name of the client connection as a string if a name is set, or null if * no name is assigned. * @example @@ -124,7 +156,7 @@ public interface ConnectionManagementClusterCommands { /** * Gets the name of the current connection. * - * @see redis.io for details. + * @see valkey.io for details. * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. * @return A {@link ClusterValue} which holds a single value if single node route is used or a @@ -146,7 +178,7 @@ public interface ConnectionManagementClusterCommands { * Echoes the provided message back.
* The command will be routed a random node. * - * @see redis.io for details. + * @see valkey.io for details. * @param message The message to be echoed back. * @return The provided message. * @example @@ -157,10 +189,25 @@ public interface ConnectionManagementClusterCommands { */ CompletableFuture echo(String message); + /** + * Echoes the provided message back.
+ * The command will be routed a random node. + * + * @see valkey.io for details. + * @param message The message to be echoed back. + * @return The provided message. + * @example + *
{@code
+     * GlideString payload = client.echo(gs("GLIDE")).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture echo(GlideString message); + /** * Echoes the provided message back. * - * @see redis.io for details. + * @see valkey.io for details. * @param message The message to be echoed back. * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. @@ -179,4 +226,27 @@ public interface ConnectionManagementClusterCommands { * }
*/ CompletableFuture> echo(String message, Route route); + + /** + * Echoes the provided message back. + * + * @see valkey.io for details. + * @param message The message to be echoed back. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The provided message. + * @example + *
{@code
+     * // Command sent to a single random node via RANDOM route, expecting a SingleValue result.
+     * GlideString message = client.echo(gs("GLIDE"), RANDOM).get().getSingleValue();
+     * assert message.equals(gs("GLIDE"));
+     *
+     * // Command sent to all nodes via ALL_NODES route, expecting a MultiValue result.
+     * Map msgForAllNodes = client.echo(gs("GLIDE"), ALL_NODES).get().getMultiValue();
+     * for(var msgPerNode : msgForAllNodes.entrySet()) {
+     *     assert msgPerNode.equals(gs("GLIDE"));
+     * }
+     * }
+ */ + CompletableFuture> echo(GlideString message, Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java index 10d5620eb9..ccdd88bdc3 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java @@ -1,19 +1,20 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import java.util.concurrent.CompletableFuture; /** * Supports commands and transactions for the "Connection Management" group for a standalone client. * - * @see Connection Management Commands + * @see Connection Management Commands */ public interface ConnectionManagementCommands { /** - * Pings the Redis server. + * Pings the server. * - * @see redis.io for details. + * @see valkey.io for details. * @return String with "PONG". * @example *
{@code
@@ -24,9 +25,9 @@ public interface ConnectionManagementCommands {
     CompletableFuture ping();
 
     /**
-     * Pings the Redis server.
+     * Pings the server.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param message The server will respond with a copy of the message.
      * @return String with a copy of the argument message.
      * @example
@@ -37,10 +38,24 @@ public interface ConnectionManagementCommands {
      */
     CompletableFuture ping(String message);
 
+    /**
+     * Pings the server.
+     *
+     * @see valkey.io for details.
+     * @param message The server will respond with a copy of the message.
+     * @return GlideString with a copy of the argument message.
+     * @example
+     *     
{@code
+     * GlideString payload = client.ping(gs("GLIDE")).get();
+     * assert payload.equals(gs("GLIDE"));
+     * }
+ */ + CompletableFuture ping(GlideString message); + /** * Gets the current connection id. * - * @see redis.io for details. + * @see valkey.io for details. * @return The id of the client. * @example *
{@code
@@ -53,7 +68,7 @@ public interface ConnectionManagementCommands {
     /**
      * Gets the name of the current connection.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @return The name of the client connection as a string if a name is set, or null if
      *     no name is assigned.
      * @example
@@ -67,7 +82,7 @@ public interface ConnectionManagementCommands {
     /**
      * Echoes the provided message back.
      *
-     * @see valkey.io for details.
      * @param message The message to be echoed back.
      * @return The provided message.
      * @example
@@ -77,4 +92,18 @@ public interface ConnectionManagementCommands {
      * }
*/ CompletableFuture echo(String message); + + /** + * Echoes the provided message back. + * + * @see + */ + CompletableFuture echo(GlideString message); } diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index 620d3412ab..a2d4c92a7d 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -1,24 +1,32 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.Script; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.RestoreOptions; import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** * Supports commands and transactions for the "Generic Commands" group for standalone and cluster * clients. * - * @see Generic Commands + * @see Generic Commands */ public interface GenericBaseCommands { + /** Valkey API keyword used to replace the destination key. */ + String REPLACE_VALKEY_API = "REPLACE"; /** * Removes the specified keys from the database. A key is ignored if it does not * exist. * - * @see redis.io for details. + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. * @param keys The keys we wanted to remove. * @return The number of keys that were removed. * @example @@ -29,10 +37,29 @@ public interface GenericBaseCommands { */ CompletableFuture del(String[] keys); + /** + * Removes the specified keys from the database. A key is ignored if it does not + * exist. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The keys we wanted to remove. + * @return The number of keys that were removed. + * @example + *
{@code
+     * Long num = client.del(new GlideString[] {gs("key1"), gs("key2")}).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture del(GlideString[] keys); + /** * Returns the number of keys in keys that exist in the database. * - * @see redis.io for details. + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. * @param keys The keys list to check. * @return The number of keys that exist. If the same existing key is mentioned in keys * multiple times, it will be counted multiple times. @@ -44,23 +71,61 @@ public interface GenericBaseCommands { */ CompletableFuture exists(String[] keys); + /** + * Returns the number of keys in keys that exist in the database. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The keys list to check. + * @return The number of keys that exist. If the same existing key is mentioned in keys + * multiple times, it will be counted multiple times. + * @example + *
{@code
+     * Long result = client.exists(new GlideString[] {gs("my_key"), gs("invalid_key")}).get();
+     * assert result == 1L;
+     * }
+ */ + CompletableFuture exists(GlideString[] keys); + /** * Unlink (delete) multiple keys from the database. A key is ignored if it does not - * exist. This command, similar to DEL, removes + * exist. This command, similar to DEL, removes * specified keys and ignores non-existent ones. However, this command does not block the server, - * while DEL does. + * while DEL does. * - * @see redis.io for details. + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. * @param keys The list of keys to unlink. * @return The number of keys that were unlinked. * @example *
{@code
-     * Long result = client.unlink("my_key").get();
+     * Long result = client.unlink(new String[] {"my_key"}).get();
      * assert result == 1L;
      * }
*/ CompletableFuture unlink(String[] keys); + /** + * Unlink (delete) multiple keys from the database. A key is ignored if it does not + * exist. This command, similar to DEL, removes + * specified keys and ignores non-existent ones. However, this command does not block the server, + * while DEL does. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The list of keys to unlink. + * @return The number of keys that were unlinked. + * @example + *
{@code
+     * Long result = client.unlink(new GlideString[] {gs("my_key")}).get();
+     * assert result == 1L;
+     * }
+ */ + CompletableFuture unlink(GlideString[] keys); + /** * Sets a timeout on key in seconds. After the timeout has expired, the key * will automatically be deleted.
@@ -71,7 +136,7 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param seconds The timeout in seconds. * @return true if the timeout was set. false if the timeout was not @@ -94,7 +159,30 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.expire(gs("my_key"), 60).get();
+     * assert isSet; //Indicates that a timeout of 60 seconds has been set for gs("my_key").
+     * }
+ */ + CompletableFuture expire(GlideString key, long seconds); + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted.
+ * If key already has an existing expire + * set, the time to live is updated to the new value.
+ * If seconds is a non-positive number, the key will be deleted rather + * than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. * @param key The key to set timeout on it. * @param seconds The timeout in seconds. * @param expireOptions The expire options. @@ -104,11 +192,36 @@ public interface GenericBaseCommands { * @example *
{@code
      * Boolean isSet = client.expire("my_key", 60, ExpireOptions.HAS_NO_EXPIRY).get();
-     * assert isSet; //Indicates that a timeout of 60 seconds has been set for "my_key."
+     * assert isSet; //Indicates that a timeout of 60 seconds has been set for "my_key".
      * }
*/ CompletableFuture expire(String key, long seconds, ExpireOptions expireOptions); + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted.
+ * If key already has an existing expire + * set, the time to live is updated to the new value.
+ * If seconds is a non-positive number, the key will be deleted rather + * than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.expire(gs("my_key"), 60, ExpireOptions.HAS_NO_EXPIRY).get();
+     * assert isSet; //Indicates that a timeout of 60 seconds has been set for gs("my_key").
+     * }
+ */ + CompletableFuture expire(GlideString key, long seconds, ExpireOptions expireOptions); + /** * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January * 1, 1970) instead of specifying the number of seconds.
@@ -119,7 +232,7 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixSeconds The timeout in an absolute Unix timestamp. * @return true if the timeout was set. false if the timeout was not @@ -142,7 +255,30 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.expireAt(gs("my_key"), Instant.now().getEpochSecond() + 10).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture expireAt(GlideString key, long unixSeconds); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixSeconds The timeout in an absolute Unix timestamp. * @param expireOptions The expire options. @@ -157,6 +293,32 @@ public interface GenericBaseCommands { */ CompletableFuture expireAt(String key, long unixSeconds, ExpireOptions expireOptions); + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.expireAt(gs("my_key"), Instant.now().getEpochSecond() + 10, ExpireOptions.HasNoExpiry).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture expireAt( + GlideString key, long unixSeconds, ExpireOptions expireOptions); + /** * Sets a timeout on key in milliseconds. After the timeout has expired, the * key will automatically be deleted.
@@ -167,7 +329,7 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param milliseconds The timeout in milliseconds. * @return true if the timeout was set. false if the timeout was not @@ -180,6 +342,29 @@ public interface GenericBaseCommands { */ CompletableFuture pexpire(String key, long milliseconds); + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted.
+ * If key already has an existing + * expire set, the time to live is updated to the new value.
+ * If milliseconds is a non-positive number, the key will be deleted + * rather than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.pexpire(gs("my_key"), 60000).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpire(GlideString key, long milliseconds); + /** * Sets a timeout on key in milliseconds. After the timeout has expired, the * key will automatically be deleted.
@@ -190,7 +375,7 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param milliseconds The timeout in milliseconds. * @param expireOptions The expire options. @@ -205,6 +390,32 @@ public interface GenericBaseCommands { */ CompletableFuture pexpire(String key, long milliseconds, ExpireOptions expireOptions); + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is updated to the new + * value.
+ * If milliseconds is a non-positive number, the key will be deleted + * rather than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.pexpire(gs("my_key"), 60000, ExpireOptions.HasNoExpiry).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpire( + GlideString key, long milliseconds, ExpireOptions expireOptions); + /** * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since * January 1, 1970) instead of specifying the number of milliseconds.
@@ -215,7 +426,7 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixMilliseconds The timeout in an absolute Unix timestamp. * @return true if the timeout was set. false if the timeout was not @@ -238,7 +449,30 @@ public interface GenericBaseCommands { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.pexpireAt(gs("my_key"), Instant.now().toEpochMilli() + 10).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpireAt(GlideString key, long unixMilliseconds); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixMilliseconds The timeout in an absolute Unix timestamp. * @param expireOptions The expire option. @@ -254,13 +488,39 @@ public interface GenericBaseCommands { CompletableFuture pexpireAt( String key, long unixMilliseconds, ExpireOptions expireOptions); + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see valkey.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire option. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.pexpireAt(gs("my_key"), Instant.now().toEpochMilli() + 10, ExpireOptions.HasNoExpiry).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpireAt( + GlideString key, long unixMilliseconds, ExpireOptions expireOptions); + /** * Returns the remaining time to live of key that has a timeout, in seconds. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to return its timeout. * @return TTL in seconds, -2 if key does not exist, or -1 - * if key exists but has no associated expire. + * if key exists but has no associated expiration. * @example *
{@code
      * Long timeRemaining = client.ttl("my_key").get();
@@ -272,21 +532,111 @@ CompletableFuture pexpireAt(
      */
     CompletableFuture ttl(String key);
 
+    /**
+     * Returns the remaining time to live of key that has a timeout, in seconds.
+     *
+     * @see valkey.io for details.
+     * @param key The key to return its timeout.
+     * @return TTL in seconds, -2 if key does not exist, or -1
+     *     if key exists but has no associated expiration.
+     * @example
+     *     
{@code
+     * Long timeRemaining = client.ttl(gs("my_key")).get();
+     * assert timeRemaining == 3600L; //Indicates that gs("my_key") has a remaining time to live of 3600 seconds.
+     *
+     * Long timeRemaining = client.ttl(gs("nonexistent_key")).get();
+     * assert timeRemaining == -2L; //Returns -2 for a non-existing key.
+     * }
+ */ + CompletableFuture ttl(GlideString key); + + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key + * will expire, in seconds.
+ * To get the expiration with millisecond precision, use {@link #pexpiretime(String)}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key to determine the expiration value of. + * @return The expiration Unix timestamp in seconds. -2 if key does not + * exist, or -1 if key exists but has no associated expiration. + * @example + *
{@code
+     * Long expiration = client.expiretime("my_key").get();
+     * System.out.printf("The key expires at %d epoch time", expiration);
+     * }
+ */ + CompletableFuture expiretime(String key); + + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key + * will expire, in seconds.
+ * To get the expiration with millisecond precision, use {@link #pexpiretime(String)}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key to determine the expiration value of. + * @return The expiration Unix timestamp in seconds. -2 if key does not + * exist, or -1 if key exists but has no associated expiration. + * @example + *
{@code
+     * Long expiration = client.expiretime(gs("my_key")).get();
+     * System.out.printf("The key expires at %d epoch time", expiration);
+     * }
+ */ + CompletableFuture expiretime(GlideString key); + + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key + * will expire, in milliseconds. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key to determine the expiration value of. + * @return The expiration Unix timestamp in milliseconds. -2 if key does + * not exist, or -1 if key exists but has no associated expiration. + * @example + *
{@code
+     * Long expiration = client.pexpiretime("my_key").get();
+     * System.out.printf("The key expires at %d epoch time (ms)", expiration);
+     * }
+ */ + CompletableFuture pexpiretime(String key); + + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key + * will expire, in milliseconds. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key to determine the expiration value of. + * @return The expiration Unix timestamp in milliseconds. -2 if key does + * not exist, or -1 if key exists but has no associated expiration. + * @example + *
{@code
+     * Long expiration = client.pexpiretime(gs("my_key")).get();
+     * System.out.printf("The key expires at %d epoch time (ms)", expiration);
+     * }
+ */ + CompletableFuture pexpiretime(GlideString key); + + // TODO move invokeScript to ScriptingAndFunctionsBaseCommands + // TODO add note to invokeScript about routing on cluster client /** * Invokes a Lua script.
- * This method simplifies the process of invoking scripts on a Redis server by using an object - * that represents a Lua script. The script loading and execution will all be handled internally. - * If the script has not already been loaded, it will be loaded automatically using the Redis - * SCRIPT LOAD command. After that, it will be invoked using the Redis EVALSHA - * command. - * - * @see SCRIPT LOAD and EVALSHA for details. + * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading and execution will all be handled internally. If + * the script has not already been loaded, it will be loaded automatically using the + * SCRIPT LOAD command. After that, it will be invoked using the EVALSHA + * command. + * + * @see SCRIPT LOAD and EVALSHA for details. * @param script The Lua script to execute. * @return a value that depends on the script that was executed. * @example *
{@code
-     * try(Script luaScript = new Script("return 'Hello'")) {
+     * try(Script luaScript = new Script("return 'Hello'", false)) {
      *     String result = (String) client.invokeScript(luaScript).get();
      *     assert result.equals("Hello");
      * }
@@ -296,20 +646,20 @@ CompletableFuture pexpireAt(
 
     /**
      * Invokes a Lua script with its keys and arguments.
- * This method simplifies the process of invoking scripts on a Redis server by using an object - * that represents a Lua script. The script loading, argument preparation, and execution will all - * be handled internally. If the script has not already been loaded, it will be loaded - * automatically using the Redis SCRIPT LOAD command. After that, it will be invoked - * using the Redis EVALSHA command. - * - * @see SCRIPT LOAD and EVALSHA for details. + * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading, argument preparation, and execution will all be + * handled internally. If the script has not already been loaded, it will be loaded automatically + * using the SCRIPT LOAD command. After that, it will be invoked using the + * EVALSHA command. + * + * @see SCRIPT LOAD and EVALSHA for details. * @param script The Lua script to execute. * @param options The script option that contains keys and arguments for the script. * @return a value that depends on the script that was executed. * @example *
{@code
-     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }")) {
+     * try(Script luaScript = new Script("return { KEYS[1], ARGV[1] }", false)) {
      *     ScriptOptions scriptOptions = ScriptOptions.builder().key("foo").arg("bar").build();
      *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions).get();
      *     assert result[0].equals("foo");
@@ -319,10 +669,35 @@ CompletableFuture pexpireAt(
      */
     CompletableFuture invokeScript(Script script, ScriptOptions options);
 
+    /**
+     * Invokes a Lua script with its keys and arguments.
+ * This method simplifies the process of invoking scripts on the server by using an object that + * represents a Lua script. The script loading, argument preparation, and execution will all be + * handled internally. If the script has not already been loaded, it will be loaded automatically + * using the SCRIPT LOAD command. After that, it will be invoked using the + * EVALSHA command. + * + * @see SCRIPT LOAD and EVALSHA for details. + * @param script The Lua script to execute. + * @param options The script option that contains keys and arguments for the script. + * @return a value that depends on the script that was executed. + * @example + *
{@code
+     * try(Script luaScript = new Script(gs("return { KEYS[1], ARGV[1] }", true))) {
+     *     ScriptOptionsGlideString scriptOptions = ScriptOptionsGlideString.builder().key(gs("foo")).arg(gs("bar")).build();
+     *     Object[] result = (Object[]) client.invokeScript(luaScript, scriptOptions).get();
+     *     assert result[0].equals(gs("foo"));
+     *     assert result[1].equals(gs("bar"));
+     * }
+     * }
+ */ + CompletableFuture invokeScript(Script script, ScriptOptionsGlideString options); + /** * Returns the remaining time to live of key that has a timeout, in milliseconds. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to return its timeout. * @return TTL in milliseconds. -2 if key does not exist, -1 * if key exists but has no associated expire. @@ -337,12 +712,30 @@ CompletableFuture pexpireAt( */ CompletableFuture pttl(String key); + /** + * Returns the remaining time to live of key that has a timeout, in milliseconds. + * + * @see valkey.io for details. + * @param key The key to return its timeout. + * @return TTL in milliseconds. -2 if key does not exist, -1 + * if key exists but has no associated expire. + * @example + *
{@code
+     * Long timeRemainingMS = client.pttl(gs("my_key")).get()
+     * assert timeRemainingMS == 5000L // Indicates that gs("my_key") has a remaining time to live of 5000 milliseconds.
+     *
+     * Long timeRemainingMS = client.pttl(gs("nonexistent_key")).get();
+     * assert timeRemainingMS == -2L; // Returns -2 for a non-existing key.
+     * }
+ */ + CompletableFuture pttl(GlideString key); + /** * Removes the existing timeout on key, turning the key from volatile (a * key with an expire set) to persistent (a key that will never expire * as no timeout is associated). * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to remove the existing timeout on. * @return false if key does not exist or does not have an associated * timeout, true if the timeout has been removed. @@ -354,10 +747,27 @@ CompletableFuture pexpireAt( */ CompletableFuture persist(String key); + /** + * Removes the existing timeout on key, turning the key from volatile (a + * key with an expire set) to persistent (a key that will never expire + * as no timeout is associated). + * + * @see valkey.io for details. + * @param key The key to remove the existing timeout on. + * @return false if key does not exist or does not have an associated + * timeout, true if the timeout has been removed. + * @example + *
{@code
+     * Boolean timeoutRemoved = client.persist(gs("my_key")).get();
+     * assert timeoutRemoved; // Indicates that the timeout associated with the key "my_key" was successfully removed.
+     * }
+ */ + CompletableFuture persist(GlideString key); + /** * Returns the string representation of the type of the value stored at key. * - * @see valkey.io for details. * @param key The key to check its data type. * @return If the key exists, the type of the stored value is returned. Otherwise, a * "none" string is returned. @@ -371,4 +781,566 @@ CompletableFuture pexpireAt( * } */ CompletableFuture type(String key); + + /** + * Returns the string representation of the type of the value stored at key. + * + * @see valkey.io for details. + * @param key The key to check its data type. + * @return If the key exists, the type of the stored value is returned. Otherwise, a + * "none" string is returned. + * @example + *
{@code
+     * String type = client.type(gs("StringKey")).get();
+     * assert type.equals("string");
+     *
+     * type = client.type(gs("ListKey")).get();
+     * assert type.equals("list");
+     * }
+ */ + CompletableFuture type(GlideString key); + + /** + * Returns the internal encoding for the Valkey object stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the internal encoding of. + * @return If key exists, returns the internal encoding of the object stored at + * key as a String. Otherwise, returns null. + * @example + *
{@code
+     * String encoding = client.objectEncoding("my_hash").get();
+     * assert encoding.equals("listpack");
+     *
+     * encoding = client.objectEncoding("non_existing_key").get();
+     * assert encoding == null;
+     * }
+ */ + CompletableFuture objectEncoding(String key); + + /** + * Returns the internal encoding for the Valkey object stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the internal encoding of. + * @return If key exists, returns the internal encoding of the object stored at + * key as a String. Otherwise, returns null. + * @example + *
{@code
+     * String encoding = client.objectEncoding(gs("my_hash")).get();
+     * assert encoding.equals("listpack");
+     *
+     * encoding = client.objectEncoding(gs("non_existing_key")).get();
+     * assert encoding == null;
+     * }
+ */ + CompletableFuture objectEncoding(GlideString key); + + /** + * Returns the logarithmic access frequency counter of a Valkey object stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the logarithmic access frequency counter + * of. + * @return If key exists, returns the logarithmic access frequency counter of the + * object stored at key as a Long. Otherwise, returns null + * . + * @example + *
{@code
+     * Long frequency = client.objectFreq("my_hash").get();
+     * assert frequency == 2L;
+     *
+     * frequency = client.objectFreq("non_existing_key").get();
+     * assert frequency == null;
+     * }
+ */ + CompletableFuture objectFreq(String key); + + /** + * Returns the logarithmic access frequency counter of a Valkey object stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the logarithmic access frequency counter + * of. + * @return If key exists, returns the logarithmic access frequency counter of the + * object stored at key as a Long. Otherwise, returns null + * . + * @example + *
{@code
+     * Long frequency = client.objectFreq(gs("my_hash")).get();
+     * assert frequency == 2L;
+     *
+     * frequency = client.objectFreq(gs("non_existing_key")).get();
+     * assert frequency == null;
+     * }
+ */ + CompletableFuture objectFreq(GlideString key); + + /** + * Returns the time in seconds since the last access to the value stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the idle time of. + * @return If key exists, returns the idle time in seconds. Otherwise, returns + * null. + * @example + *
{@code
+     * Long idletime = client.objectIdletime("my_hash").get();
+     * assert idletime == 2L;
+     *
+     * idletime = client.objectIdletime("non_existing_key").get();
+     * assert idletime == null;
+     * }
+ */ + CompletableFuture objectIdletime(String key); + + /** + * Returns the time in seconds since the last access to the value stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the idle time of. + * @return If key exists, returns the idle time in seconds. Otherwise, returns + * null. + * @example + *
{@code
+     * Long idletime = client.objectIdletime(gs("my_hash")).get();
+     * assert idletime == 2L;
+     *
+     * idletime = client.objectIdletime(gs("non_existing_key")).get();
+     * assert idletime == null;
+     * }
+ */ + CompletableFuture objectIdletime(GlideString key); + + /** + * Returns the reference count of the object stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the reference count of. + * @return If key exists, returns the reference count of the object stored at + * key as a Long. Otherwise, returns null. + * @example + *
{@code
+     * Long refcount = client.objectRefcount("my_hash").get();
+     * assert refcount == 2L;
+     *
+     * refcount = client.objectRefcount("non_existing_key").get();
+     * assert refcount == null;
+     * }
+ */ + CompletableFuture objectRefcount(String key); + + /** + * Returns the reference count of the object stored at key. + * + * @see valkey.io for details. + * @param key The key of the object to get the reference count of. + * @return If key exists, returns the reference count of the object stored at + * key as a Long. Otherwise, returns null. + * @example + *
{@code
+     * Long refcount = client.objectRefcount(gs("my_hash")).get();
+     * assert refcount == 2L;
+     *
+     * refcount = client.objectRefcount(gs("non_existing_key")).get();
+     * assert refcount == null;
+     * }
+ */ + CompletableFuture objectRefcount(GlideString key); + + /** + * Renames key to newKey.
+ * If newKey already exists it is overwritten. + * + * @apiNote When in cluster mode, both key and newKey must map to the + * same hash slot. + * @see valkey.io for details. + * @param key The key to rename. + * @param newKey The new name of the key. + * @return If the key was successfully renamed, return "OK". If + * key does not exist, an error is thrown. + * @example + *
{@code
+     * String value = client.set("key", "value").get();
+     * value = client.rename("key", "newKeyName").get();
+     * assert value.equals("OK");
+     * }
+ */ + CompletableFuture rename(String key, String newKey); + + /** + * Renames key to newKey.
+ * If newKey already exists it is overwritten. + * + * @apiNote When in cluster mode, both key and newKey must map to the + * same hash slot. + * @see valkey.io for details. + * @param key The key to rename. + * @param newKey The new name of the key. + * @return If the key was successfully renamed, return "OK". If + * key does not exist, an error is thrown. + * @example + *
{@code
+     * String value = client.set(gs("key"), gs("value")).get();
+     * value = client.rename(gs("key"), gs("newKeyName")).get();
+     * assert value.equals("OK");
+     * }
+ */ + CompletableFuture rename(GlideString key, GlideString newKey); + + /** + * Renames key to newKey if newKey does not yet exist. + * + * @apiNote When in cluster mode, both key and newKey must map to the + * same hash slot. + * @see valkey.io for details. + * @param key The key to rename. + * @param newKey The new key name. + * @return true if key was renamed to newKey, false + * if newKey already exists. + * @example + *
{@code
+     * Boolean renamed = client.renamenx("old_key", "new_key").get();
+     * assert renamed;
+     * }
+ */ + CompletableFuture renamenx(String key, String newKey); + + /** + * Renames key to newKey if newKey does not yet exist. + * + * @apiNote When in cluster mode, both key and newKey must map to the + * same hash slot. + * @see valkey.io for details. + * @param key The key to rename. + * @param newKey The new key name. + * @return true if key was renamed to newKey, false + * if newKey already exists. + * @example + *
{@code
+     * Boolean renamed = client.renamenx(gs("old_key"), gs("new_key")).get();
+     * assert renamed;
+     * }
+ */ + CompletableFuture renamenx(GlideString key, GlideString newKey); + + /** + * Updates the last access time of specified keys. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The keys to update last access time. + * @return The number of keys that were updated. + * @example + *
{@code
+     * Long payload = client.touch(new String[] {"myKey1", "myKey2", "nonExistentKey"}).get();
+     * assert payload == 2L; // Last access time of 2 keys has been updated.
+     * }
+ */ + CompletableFuture touch(String[] keys); + + /** + * Updates the last access time of specified keys. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The keys to update last access time. + * @return The number of keys that were updated. + * @example + *
{@code
+     * Long payload = client.touch(new GlideString[] {gs("myKey1"), gs("myKey2"), gs("nonExistentKey")}).get();
+     * assert payload == 2L; // Last access time of 2 keys has been updated.
+     * }
+ */ + CompletableFuture touch(GlideString[] keys); + + /** + * Copies the value stored at the source to the destination key if the + * destination key does not yet exist. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * client.set("test2", "two").get();
+     * assert !client.copy("test1", "test2").get();
+     * assert client.copy("test1", "test2").get();
+     * }
+ */ + CompletableFuture copy(String source, String destination); + + /** + * Copies the value stored at the source to the destination key if the + * destination key does not yet exist. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * client.set(gs("test2"), gs("two")).get();
+     * assert !client.copy(gs("test1", gs("test2")).get();
+     * assert client.copy(gs("test1"), gs("test2")).get();
+     * }
+ */ + CompletableFuture copy(GlideString source, GlideString destination); + + /** + * Copies the value stored at the source to the destination key. When + * replace is true, removes the destination key first if it already + * exists, otherwise performs no action. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * client.set("test2", "two").get();
+     * assert !client.copy("test1", "test2", false).get();
+     * assert client.copy("test1", "test2", true).get();
+     * }
+ */ + CompletableFuture copy(String source, String destination, boolean replace); + + /** + * Copies the value stored at the source to the destination key. When + * replace is true, removes the destination key first if it already + * exists, otherwise performs no action. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * client.set(gs("test2"), gs("two")).get();
+     * assert !client.copy(gs("test1", gs("test2"), false).get();
+     * assert client.copy(gs("test1", gs("test2"), true).get();
+     * }
+ */ + CompletableFuture copy(GlideString source, GlideString destination, boolean replace); + + /** + * Serialize the value stored at key in a Valkey-specific format and return it to the + * user. + * + * @see valkey.io for details. + * @param key The key of the set. + * @return The serialized value of a set.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * byte[] result = client.dump("myKey").get();
+     *
+     * byte[] response = client.dump("nonExistingKey").get();
+     * assert response.equals(null);
+     * }
+ */ + CompletableFuture dump(GlideString key); + + /** + * Create a key associated with a value that is obtained by + * deserializing the provided serialized value (obtained via {@link #dump}). + * + * @see valkey.io for details. + * @param key The key of the set. + * @param ttl The expiry time (in milliseconds). If 0, the key will + * persist. + * @param value The serialized value. + * @return Return OK if successfully create a key with a value + * . + * @example + *
{@code
+     * String result = client.restore(gs("newKey"), 0, value).get();
+     * assert result.equals("OK");
+     * }
+ */ + CompletableFuture restore(GlideString key, long ttl, byte[] value); + + /** + * Create a key associated with a value that is obtained by + * deserializing the provided serialized value (obtained via {@link #dump}). + * + * @see valkey.io for details. + * @param key The key of the set. + * @param ttl The expiry time (in milliseconds). If 0, the key will + * persist. + * @param value The serialized value. + * @param restoreOptions The restore options. See {@link RestoreOptions}. + * @return Return OK if successfully create a key with a value + * . + * @example + *
{@code
+     * RestoreOptions options = RestoreOptions.builder().replace().absttl().idletime(10).frequency(10).build()).get();
+     * // Set restore options with replace and absolute TTL modifiers, object idletime and frequency to 10.
+     * String result = client.restore(gs("newKey"), 0, value, options).get();
+     * assert result.equals("OK");
+     * }
+ */ + CompletableFuture restore( + GlideString key, long ttl, byte[] value, RestoreOptions restoreOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String)}.
+ * + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sort("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sort(String key); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String)}.
+ * + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush(gs("mylist"), new GlideString[] {gs("3"), gs("1"), gs("2")}).get();
+     * assertArrayEquals(new GlideString[] {gs("1"), gs("2"), gs("3")}, client.sort(gs("mylist")).get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sort(GlideString key); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sortReadOnly("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sortReadOnly(String key); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush(gs("mylist", new GlideString[] {gs("3"), gs("1"), gs("2")}).get();
+     * assertArrayEquals(new GlideString[] {gs("1"), gs("2"), gs("3")}, client.sortReadOnly(gs("mylist")).get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sortReadOnly(GlideString key); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String)} or {@link + * #sortReadOnly(String)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assert client.sortStore("mylist", "destination").get() == 3;
+     * assertArrayEquals(
+     *    new String[] {"1", "2", "3"},
+     *    client.lrange("destination", 0, -1).get()); // Sorted list is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(GlideString)} or {@link + * #sortReadOnly(GlideString)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush(gs("mylist"), new GlideString[] {gs("3"), gs("1"), gs("2")}).get();
+     * assert client.sortStore(gs("mylist"), gs("destination")).get() == 3;
+     * assertArrayEquals(
+     *    new GlideString[] {gs("1"), gs("2"), gs("3")},
+     *    client.lrange(gs("destination"), 0, -1).get()); // Sorted list is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(GlideString key, GlideString destination); + + /** + * Blocks the current client until all the previous write commands are successfully transferred + * and acknowledged by at least numreplicas of replicas. If timeout is + * reached, the command returns even if the specified number of replicas were not yet reached. + * + * @param numreplicas The number of replicas to reach. + * @param timeout The timeout value specified in milliseconds. A value of 0 will + * block indefinitely. + * @return The number of replicas reached by all the writes performed in the context of the + * current connection. + * @example + *
{@code
+     * client.set("key", "value).get();
+     * assert client.wait(1L, 1000L).get() == 1L;
+     * }
+ */ + CompletableFuture wait(long numreplicas, long timeout); } diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index 8f89590373..544c40a2c1 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -1,9 +1,14 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.Transaction; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.scan.ClusterScanCursor; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import java.util.concurrent.CompletableFuture; @@ -11,7 +16,7 @@ /** * Supports commands for the "Generic Commands" group for a cluster client. * - * @see Generic Commands + * @see Generic Commands */ public interface GenericClusterCommands { @@ -21,13 +26,11 @@ public interface GenericClusterCommands { * *

The command will be routed to all primaries. * - * @remarks This function should only be used for single-response commands. Commands that don't - * return response (such as SUBSCRIBE), or that return potentially more than a single - * response (such as XREAD), or that change the client's behavior (such as entering - * pub/sub mode on RESP2 connections) shouldn't be called using - * this function. + * @apiNote See Valkey + * GLIDE Wiki for details on the restrictions and limitations of the custom command API. * @param args Arguments for the custom command including the command name. - * @return Response from Redis containing an Object. + * @return The returning value depends on the executed command. * @example *

{@code
      * ClusterValue data = client.customCommand(new String[] {"ping"}).get();
@@ -42,15 +45,13 @@ public interface GenericClusterCommands {
      *
      * 

Client will route the command to the nodes defined by route. * - * @remarks This function should only be used for single-response commands. Commands that don't - * return response (such as SUBSCRIBE), or that return potentially more than a single - * response (such as XREAD), or that change the client's behavior (such as entering - * pub/sub mode on RESP2 connections) shouldn't be called using - * this function. + * @apiNote See Valkey + * GLIDE Wiki for details on the restrictions and limitations of the custom command API. * @param args Arguments for the custom command including the command name * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. - * @return Response from Redis containing an Object. + * @return The returning value depends on the executed command and route. * @example *

{@code
      * ClusterValue result = clusterClient.customCommand(new String[]{ "CONFIG", "GET", "maxmemory"}, ALL_NODES).get();
@@ -67,7 +68,7 @@ public interface GenericClusterCommands {
      * 

The transaction will be routed to the slot owner of the first key found in the transaction. * If no key is found, the command will be sent to a random node. * - * @see redis.io for details on Redis + * @see valkey.io for details on * Transactions. * @param transaction A {@link Transaction} object containing a list of commands to be executed. * @return A list of results corresponding to the execution of each command in the transaction. @@ -91,7 +92,7 @@ public interface GenericClusterCommands { /** * Executes a transaction by processing the queued commands. * - * @see redis.io for details on Redis + * @see valkey.io for details on * Transactions. * @param transaction A {@link Transaction} object containing a list of commands to be executed. * @param route A single-node routing configuration for the transaction. The client will route the @@ -114,4 +115,456 @@ public interface GenericClusterCommands { * } */ CompletableFuture exec(ClusterTransaction transaction, SingleNodeRoute route); + + /** + * Returns a random key. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route, and will return the first successful + * result. + * @return A random key from the database. + * @example + *

{@code
+     * String value = client.set("key", "value").get();
+     * String value_1 = client.set("key1", "value_1").get();
+     * String key = client.randomKey(RANDOM).get();
+     * System.out.println("The random key is: " + key);
+     * // The value of key is either "key" or "key1"
+     * }
+ */ + CompletableFuture randomKey(Route route); + + /** + * Returns a random key. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route, and will return the first successful + * result. + * @return A random key from the database. + * @example + *
{@code
+     * String value = client.set("key", "value").get();
+     * String value_1 = client.set("key1", "value_1").get();
+     * GlideString key = client.randomKeyBinary(RANDOM).get();
+     * System.out.println("The random key is: " + key);
+     * // The value of key is either "key" or "key1"
+     * }
+ */ + CompletableFuture randomKeyBinary(Route route); + + /** + * Returns a random key.
+ * The command will be routed to all primary nodes, and will return the first successful result. + * + * @see valkey.io for details. + * @return A random key from the database. + * @example + *
{@code
+     * String value = client.set("key", "value").get();
+     * String value_1 = client.set("key1", "value_1").get();
+     * String key = client.randomKey().get();
+     * System.out.println("The random key is: " + key);
+     * // The value of key is either "key" or "key1"
+     * }
+ */ + CompletableFuture randomKey(); + + /** + * Returns a random key.
+ * The command will be routed to all primary nodes, and will return the first successful result. + * + * @see valkey.io for details. + * @return A random key from the database. + * @example + *
{@code
+     * String value = client.set(gs("key"),gs( "value")).get();
+     * String value_1 = client.set(gs("key1"), gs("value_1")).get();
+     * GlideString key = client.randomKeyBinary().get();
+     * System.out.println("The random key is: " + key);
+     * // The value of key is either "key" or "key1"
+     * }
+ */ + CompletableFuture randomKeyBinary(); + + /** + * Incrementally iterates over the keys in the Cluster. + * + *

This command is similar to the SCAN command, but it is designed to work in a + * Cluster environment. The main difference is that this command uses a {@link ClusterScanCursor} + * object to manage iterations. For more information about the Cluster Scan implementation, see Cluster + * Scan. + * + *

As with the SCAN command, this command is a cursor-based iterator. This means + * that at every call of the command, the server returns an updated cursor ({@link + * ClusterScanCursor}) that the user needs to re-send as the cursor argument in the + * next call. The iteration terminates when the returned cursor {@link + * ClusterScanCursor#isFinished()} returns true. + * + *

This method guarantees that all keyslots available when the first SCAN is called will be + * scanned before the cursor is finished. Any keys added after the initial scan request is made + * are not guaranteed to be scanned. + * + *

Note that the same key may be returned in multiple scan iterations. + * + *

How to use the {@link ClusterScanCursor}:
+ * For each iteration, the previous scan {@link ClusterScanCursor} object should be used to + * continue the SCAN by passing it in the cursor argument. Using the + * same cursor object for multiple iterations may result in the same keys returned or unexpected + * behavior. + * + *

When the cursor is no longer needed, call {@link ClusterScanCursor#releaseCursorHandle()} to + * immediately free resources tied to the cursor. Note that this makes the cursor unusable in + * subsequent calls to SCAN. + * + * @see valkey.io for details. + * @param cursor The {@link ClusterScanCursor} object that wraps the scan state. To start a new + * scan, create a new empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. + * @return An Array with two elements. The first element is always the {@link + * ClusterScanCursor} for the next iteration of results. To see if there is more data on the + * given cursor, call {@link ClusterScanCursor#isFinished()}. To release resources for the + * current cursor immediately, call {@link ClusterScanCursor#releaseCursorHandle()} after + * using the cursor in a call to this method. The cursor cannot be used in a scan again after + * {@link ClusterScanCursor#releaseCursorHandle()} has been called. The second element is an + * + * Array of String elements each representing a key. + * @example + *

{@code
+     * // Assume key contains a set with 200 keys
+     * ClusterScanCursor cursor = ClusterScanCursor.initialCursor();
+     * Object[] result;
+     * while (!cursor.isFinished()) {
+     *   result = client.scan(cursor).get();
+     *   cursor.releaseCursorHandle();
+     *   cursor = (ClusterScanCursor) result[0];
+     *   Object[] stringResults = (Object[]) result[1];
+     *   System.out.println("\nSCAN iteration:");
+     *   Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * }
+     * }
+ */ + CompletableFuture scan(ClusterScanCursor cursor); + + /** + * Incrementally iterates over the keys in the Cluster. + * + *

This command is similar to the SCAN command, but it is designed to work in a + * Cluster environment. The main difference is that this command uses a {@link ClusterScanCursor} + * object to manage iterations. For more information about the Cluster Scan implementation, see Cluster + * Scan. + * + *

As with the SCAN command, this command is a cursor-based iterator. This means + * that at every call of the command, the server returns an updated cursor ({@link + * ClusterScanCursor}) that the user needs to re-send as the cursor argument in the + * next call. The iteration terminates when the returned cursor {@link + * ClusterScanCursor#isFinished()} returns true. + * + *

This method guarantees that all keyslots available when the first SCAN is called will be + * scanned before the cursor is finished. Any keys added after the initial scan request is made + * are not guaranteed to be scanned. + * + *

Note that the same key may be returned in multiple scan iterations. + * + *

How to use the {@link ClusterScanCursor}:
+ * For each iteration, the previous scan {@link ClusterScanCursor} object should be used to + * continue the SCAN by passing it in the cursor argument. Using the + * same cursor object for multiple iterations may result in the same keys returned or unexpected + * behavior. + * + *

When the cursor is no longer needed, call {@link ClusterScanCursor#releaseCursorHandle()} to + * immediately free resources tied to the cursor. Note that this makes the cursor unusable in + * subsequent calls to SCAN. + * + * @see valkey.io for details. + * @param cursor The {@link ClusterScanCursor} object that wraps the scan state. To start a new + * scan, create a new empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. + * @return An Array with two elements. The first element is always the {@link + * ClusterScanCursor} for the next iteration of results. To see if there is more data on the + * given cursor, call {@link ClusterScanCursor#isFinished()}. To release resources for the + * current cursor immediately, call {@link ClusterScanCursor#releaseCursorHandle()} after + * using the cursor in a call to this method. The cursor cannot be used in a scan again after + * {@link ClusterScanCursor#releaseCursorHandle()} has been called. The second element is an + * Array of GlideString elements each representing a key. + * @example + *

{@code
+     * // Assume key contains a set with 200 keys
+     * ClusterScanCursor cursor = ClusterScanCursor.initialCursor();
+     * Object[] result;
+     * while (!cursor.isFinished()) {
+     *   result = client.scan(cursor).get();
+     *   cursor.releaseCursorHandle();
+     *   cursor = (ClusterScanCursor) result[0];
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *   System.out.println("\nSCAN iteration:");
+     *   Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * }
+     * }
+ */ + CompletableFuture scanBinary(ClusterScanCursor cursor); + + /** + * Incrementally iterates over the keys in the Cluster. + * + *

This command is similar to the SCAN command, but it is designed to work in a + * Cluster environment. The main difference is that this command uses a {@link ClusterScanCursor} + * object to manage iterations. For more information about the Cluster Scan implementation, see Cluster + * Scan. + * + *

As with the SCAN command, this command is a cursor-based iterator. This means + * that at every call of the command, the server returns an updated cursor ({@link + * ClusterScanCursor}) that the user needs to re-send as the cursor argument in the + * next call. The iteration terminates when the returned cursor {@link + * ClusterScanCursor#isFinished()} returns true. + * + *

This method guarantees that all keyslots available when the first SCAN is called will be + * scanned before the cursor is finished. Any keys added after the initial scan request is made + * are not guaranteed to be scanned. + * + *

Note that the same key may be returned in multiple scan iterations. + * + *

How to use the {@link ClusterScanCursor}:
+ * For each iteration, the previous scan {@link ClusterScanCursor} object should be used to + * continue the SCAN by passing it in the cursor argument. Using the + * same cursor object for multiple iterations may result in the same keys returned or unexpected + * behavior. + * + *

When the cursor is no longer needed, call {@link ClusterScanCursor#releaseCursorHandle()} to + * immediately free resources tied to the cursor. Note that this makes the cursor unusable in + * subsequent calls to SCAN. + * + * @see valkey.io for details. + * @param cursor The {@link ClusterScanCursor} object that wraps the scan state. To start a new + * scan, create a new empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. + * @param options The {@link ScanOptions}. + * @return An Array with two elements. The first element is always the {@link + * ClusterScanCursor} for the next iteration of results. To see if there is more data on the + * given cursor, call {@link ClusterScanCursor#isFinished()}. To release resources for the + * current cursor immediately, call {@link ClusterScanCursor#releaseCursorHandle()} after + * using the cursor in a call to this method. The cursor cannot be used in a scan again after + * {@link ClusterScanCursor#releaseCursorHandle()} has been called. The second element is an + * Array of String elements each representing a key. + * @example + *

{@code
+     * // Assume key contains a set with 200 keys
+     * ClusterScanCursor cursor = ClusterScanCursor.initialCursor();
+     * // Scan for keys with archived in the name
+     * ScanOptions options = ScanOptions.builder().matchPattern("*archived*").build();
+     * Object[] result;
+     * while (!cursor.isFinished()) {
+     *   result = client.scan(cursor, options).get();
+     *   cursor.releaseCursorHandle();
+     *   cursor = (ClusterScanCursor) result[0];
+     *   Object[] stringResults = (Object[]) result[1];
+     *   System.out.println("\nSCAN iteration:");
+     *   Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * }
+     * }
+ */ + CompletableFuture scan(ClusterScanCursor cursor, ScanOptions options); + + /** + * Incrementally iterates over the keys in the Cluster. + * + *

This command is similar to the SCAN command, but it is designed to work in a + * Cluster environment. The main difference is that this command uses a {@link ClusterScanCursor} + * object to manage iterations. For more information about the Cluster Scan implementation, see Cluster + * Scan. + * + *

As with the SCAN command, this command is a cursor-based iterator. This means + * that at every call of the command, the server returns an updated cursor ({@link + * ClusterScanCursor}) that the user needs to re-send as the cursor argument in the + * next call. The iteration terminates when the returned cursor {@link + * ClusterScanCursor#isFinished()} returns true. + * + *

This method guarantees that all keyslots available when the first SCAN is called will be + * scanned before the cursor is finished. Any keys added after the initial scan request is made + * are not guaranteed to be scanned. + * + *

Note that the same key may be returned in multiple scan iterations. + * + *

How to use the {@link ClusterScanCursor}:
+ * For each iteration, the previous scan {@link ClusterScanCursor} object should be used to + * continue the SCAN by passing it in the cursor argument. Using the + * same cursor object for multiple iterations may result in the same keys returned or unexpected + * behavior. + * + *

When the cursor is no longer needed, call {@link ClusterScanCursor#releaseCursorHandle()} to + * immediately free resources tied to the cursor. Note that this makes the cursor unusable in + * subsequent calls to SCAN. + * + * @see valkey.io for details. + * @param cursor The {@link ClusterScanCursor} object that wraps the scan state. To start a new + * scan, create a new empty ClusterScanCursor using {@link ClusterScanCursor#initalCursor()}. + * @param options The {@link ScanOptions}. + * @return An Array with two elements. The first element is always the {@link + * ClusterScanCursor} for the next iteration of results. To see if there is more data on the + * given cursor, call {@link ClusterScanCursor#isFinished()}. To release resources for the + * current cursor immediately, call {@link ClusterScanCursor#releaseCursorHandle()} after + * using the cursor in a call to this method. The cursor cannot be used in a scan again after + * {@link ClusterScanCursor#releaseCursorHandle()} has been called. The second element is an + * Array of GlideString elements each representing a key. + * @example + *

{@code
+     * // Assume key contains a set with 200 keys
+     * ClusterScanCursor cursor = ClusterScanCursor.initialCursor();
+     * // Scan for keys with archived in the name
+     * ScanOptions options = ScanOptions.builder().matchPattern("*archived*").build();
+     * Object[] result;
+     * while (!cursor.isFinished()) {
+     *   result = client.scan(cursor, options).get();
+     *   cursor.releaseCursorHandle();
+     *   cursor = (ClusterScanCursor) result[0];
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *   System.out.println("\nSCAN iteration:");
+     *   Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * }
+     * }
+ */ + CompletableFuture scanBinary(ClusterScanCursor cursor, ScanOptions options); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sort("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sort(String key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush(gs("mylist"), new GlideString[] {gs("3"), gs("1"), gs("2"), gs("a")}).get();
+     * GlideString[] payload = client.sort(gs("mylist"), SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new GlideString[] {gs("a"), gs("3"), gs("2")}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sort(GlideString key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sortReadOnly("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new GlideString[] {gs("3"), gs("1"), gs("2"), gs("a")}).get();
+     * GlideString[] payload = client.sortReadOnly(gs("mylist"), SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new GlideString[] {gs("a"), gs("3"), gs("2")}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sortReadOnly( + GlideString key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * Long payload = client.sortStore("mylist", "destination",
+     *          SortClusterOptions.builder().alpha().orderBy(DESC)
+     *              .limit(new SortBaseOptions.Limit(0L, 3L))build()).get();
+     * assertEquals(3, payload);
+     * assertArrayEquals(
+     *      new String[] {"a", "3", "2"},
+     *      client.lrange("destination", 0, -1).get()); // Sorted list is stored in "destination"
+     * }
+ */ + CompletableFuture sortStore( + String key, String destination, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush(gs("mylist"), new GlideString[] {gs("3"), gs("1"), gs("2"), gs("a")}).get();
+     * Long payload = client.sortStore(gs("mylist"), gs("destination"),
+     *          SortClusterOptions.builder().alpha().orderBy(DESC)
+     *              .limit(new SortBaseOptions.Limit(0L, 3L))build()).get();
+     * assertEquals(3, payload);
+     * assertArrayEquals(
+     *      new GlideString[] {gs("a"), gs("3"), gs("2")},
+     *      client.lrange(gs("destination"), 0, -1).get()); // Sorted list is stored in "destination"
+     * }
+ */ + CompletableFuture sortStore( + GlideString key, GlideString destination, SortClusterOptions sortClusterOptions); } diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java index e4bde9ce06..a40503a37e 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java @@ -1,27 +1,32 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.Transaction; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.SortOptionsBinary; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** * Supports commands and transactions for the "Generic Commands" group for a standalone client. * - * @see Generic Commands + * @see Generic Commands */ public interface GenericCommands { + /** Valkey API keyword used to denote the destination db index. */ + String DB_VALKEY_API = "DB"; /** * Executes a single command, without checking inputs. Every part of the command, including * subcommands, should be added as a separate value in args. * + * @apiNote See Valkey + * GLIDE Wiki for details on the restrictions and limitations of the custom command API. * @param args Arguments for the custom command. - * @return Response from Redis containing an Object. - * @remarks This function should only be used for single-response commands. Commands that don't - * return response (such as SUBSCRIBE), or that return potentially more than a single - * response (such as XREAD), or that change the client's behavior (such as entering - * pub/sub mode on RESP2 connections) shouldn't be called using - * this function. + * @return The returning value depends on the executed command. * @example *
{@code
      * Object response = (String) client.customCommand(new String[] {"ping", "GLIDE"}).get();
@@ -35,7 +40,7 @@ public interface GenericCommands {
     /**
      * Executes a transaction by processing the queued commands.
      *
-     * @see redis.io for details on Redis
+     * @see valkey.io for details on
      *     Transactions.
      * @param transaction A {@link Transaction} object containing a list of commands to be executed.
      * @return A list of results corresponding to the execution of each command in the transaction.
@@ -55,4 +60,422 @@ public interface GenericCommands {
      * }
*/ CompletableFuture exec(Transaction transaction); + + /** + * Move key from the currently selected database to the database specified by + * dbIndex. + * + * @see valkey.io for more details. + * @param key The key to move. + * @param dbIndex The index of the database to move key to. + * @return true if key was moved, or false if the key + * already exists in the destination database or does not exist in the source + * database. + * @example + *
{@code
+     * Boolean moved = client.move("some_key", 1L).get();
+     * assert moved;
+     * }
+ */ + CompletableFuture move(String key, long dbIndex); + + /** + * Move key from the currently selected database to the database specified by + * dbIndex. + * + * @see valkey.io for more details. + * @param key The key to move. + * @param dbIndex The index of the database to move key to. + * @return true if key was moved, or false if the key + * already exists in the destination database or does not exist in the source + * database. + * @example + *
{@code
+     * Boolean moved = client.move(gs("some_key"), 1L).get();
+     * assert moved;
+     * }
+ */ + CompletableFuture move(GlideString key, long dbIndex); + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * assert client.copy("test1", "test2", 1, false).get();
+     * }
+ */ + CompletableFuture copy( + String source, String destination, long destinationDB, boolean replace); + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * assert client.copy(gs("test1"), gs("test2"), 1, false).get();
+     * }
+ */ + CompletableFuture copy( + GlideString source, GlideString destination, long destinationDB, boolean replace); + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * assert client.copy("test1", "test2", 1).get();
+     * }
+ */ + CompletableFuture copy(String source, String destination, long destinationDB); + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set(gs("test1"), gs("one")).get();
+     * assert client.copy(gs("test1"), gs("test2"), 1).get();
+     * }
+ */ + CompletableFuture copy(GlideString source, GlideString destination, long destinationDB); + + /** + * Returns a random key from currently selected database. + * + * @see valkey.io for details. + * @return A random key from the database. + * @example + *
{@code
+     * String value = client.set("key", "value").get();
+     * String value_1 = client.set("key1", "value_1").get();
+     * String key = client.randomKey().get();
+     * System.out.println("The random key is: " + key);
+     * // The value of key is either "key" or "key1"
+     * }
+ */ + CompletableFuture randomKey(); + + /** + * Returns a random key from currently selected database. + * + * @see valkey.io for details. + * @return A random key from the database. + * @example + *
{@code
+     * String value = client.set(gs("key"), gs("value")).get();
+     * String value_1 = client.set(gs("key1"), gs("value_1")).get();
+     * String key = client.randomKeyBinary().get();
+     * System.out.println("The random key is: " + key);
+     * // The value of key is either "key" or "key1"
+     * }
+ */ + CompletableFuture randomKeyBinary(); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortOptions)}. + * + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sort("user_ids", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sort(String key, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(GlideString, GlideString, + * SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptionsBinary}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset(gs("user:1"), Map.of(gs("name"), gs("Alice"), gs("age"), gs("30"))).get();
+     * client.hset(gs("user:2"), Map.of(gs("name"), gs("Bob"), gs("age"), gs("25"))).get();
+     * client.lpush(gs("user_ids"), new GlideString[] {gs("2"), gs("1")}).get();
+     * GlideString [] payload = client.sort(gs("user_ids"), SortOptionsBinary.builder().byPattern(gs("user:*->age"))
+     *                  .getPattern(gs("user:*->name")).build()).get();
+     * assertArrayEquals(new GlideString[] {gs("Bob"), gs("Alice")}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sort(GlideString key, SortOptionsBinary sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sortReadOnly("user_ids", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset(gs("user:1"), Map.of(gs("name"), gs("Alice"), gs("age"), gs("30"))).get();
+     * client.hset(gs("user:2"), Map.of(gs("name"), gs("Bob"), gs("age"), gs("25"))).get();
+     * client.lpush("user_ids", new GlideString[] {gs("2"), gs("1")}).get();
+     * GlideString [] payload = client.sortReadOnly(gs("user_ids"), SortOptionsBinary.builder().byPattern(gs("user:*->age"))
+     *                  .getPattern(gs("user:*->name")).build()).get();
+     * assertArrayEquals(new GlideString[] {gs("Bob"), gs("Alice")}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sortReadOnly(GlideString key, SortOptionsBinary sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, SortOptions)}. + * + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * Long payload = client.sortStore("user_ids", "destination",
+     *          SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build())
+     *          .get();
+     * assertEquals(2, payload);
+     * assertArrayEquals(
+     *      new String[] {"Bob", "Alice"},
+     *      client.lrange("destination", 0, -1).get()); // The list of the names sorted by age is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(GlideString, + * SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.hset(gs("user:1"), Map.of(gs("name"), gs("Alice"), gs("age"), gs("30"))).get();
+     * client.hset(gs("user:2"), Map.of(gs("name"), gs("Bob"), gs("age"), gs("25"))).get();
+     * client.lpush(gs("user_ids"), new GlideString[] {gs("2"), gs("1")}).get();
+     * Long payload = client.sortStore(gs("user_ids"), gs("destination"),
+     *          SortOptionsBinary.builder().byPattern(gs("user:*->age")).getPattern(gs("user:*->name")).build())
+     *          .get();
+     * assertEquals(2, payload);
+     * assertArrayEquals(
+     *      new GlideString[] {gs("Bob"), gs("Alice")},
+     *      client.lrange(gs("destination"), 0, -1).get()); // The list of the names sorted by age is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore( + GlideString key, GlideString destination, SortOptionsBinary sortOptions); + + /** + * Iterates incrementally over a database for matching keys. + * + * @see valkey.io for details. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the scan.
+ * The second element is always an Array of matched keys from the database. + * @example + *
{@code
+     * // Assume database contains a set with 200 keys
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *     result = client.scan(cursor).get();
+     *     cursor = result[0].toString();
+     *     Object[] stringResults = (Object[]) result[1];
+     *     String keyList = Arrays.stream(stringResults)
+     *         .map(obj -> (String)obj)
+     *         .collect(Collectors.joining(", "));
+     *     System.out.println("\nSCAN iteration: " + keyList);
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture scan(String cursor); + + /** + * Iterates incrementally over a database for matching keys. + * + * @see valkey.io for details. + * @param cursor The cursor that points to the next iteration of results. A value of gs("0") + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. gs("0") will be the + * cursor + * returned on the last iteration of the scan.
+ * The second element is always an Array of matched keys from the database. + * @example + *
{@code
+     * // Assume database contains a set with 200 keys
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *     result = client.scan(cursor).get();
+     *     cursor = gs(result[0].toString());
+     *     Object[] stringResults = (Object[]) result[1];
+     *     String keyList = Arrays.stream(stringResults)
+     *         .map(obj -> obj.toString())
+     *         .collect(Collectors.joining(", "));
+     *     System.out.println("\nSCAN iteration: " + keyList);
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture scan(GlideString cursor); + + /** + * Iterates incrementally over a database for matching keys. + * + * @see valkey.io for details. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param options The {@link ScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the scan.
+ * The second element is always an Array of matched keys from the database. + * @example + *
{@code
+     * // Assume database contains a set with 200 keys
+     * String cursor = "0";
+     * Object[] result;
+     * // match keys on pattern *11*
+     * ScanOptions options = ScanOptions.builder().matchPattern("*11*").build();
+     * do {
+     *     result = client.scan(cursor, options).get();
+     *     cursor = result[0].toString();
+     *     Object[] stringResults = (Object[]) result[1];
+     *     String keyList = Arrays.stream(stringResults)
+     *         .map(obj -> (String)obj)
+     *         .collect(Collectors.joining(", "));
+     *     System.out.println("\nSCAN iteration: " + keyList);
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture scan(String cursor, ScanOptions options); + + /** + * Iterates incrementally over a database for matching keys. + * + * @see valkey.io for details. + * @param cursor The cursor that points to the next iteration of results. A value of gs("0") + * indicates the start of the search. + * @param options The {@link ScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. gs("0") will be the + * cursor + * returned on the last iteration of the scan.
+ * The second element is always an Array of matched keys from the database. + * @example + *
{@code
+     * // Assume database contains a set with 200 keys
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * // match keys on pattern *11*
+     * ScanOptions options = ScanOptions.builder().matchPattern("*11*").build();
+     * do {
+     *     result = client.scan(cursor, options).get();
+     *     cursor = gs(result[0].toString());
+     *     Object[] stringResults = (Object[]) result[1];
+     *     String keyList = Arrays.stream(stringResults)
+     *         .map(obj -> obj.toString())
+     *         .collect(Collectors.joining(", "));
+     *     System.out.println("\nSCAN iteration: " + keyList);
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture scan(GlideString cursor, ScanOptions options); } diff --git a/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java b/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java new file mode 100644 index 0000000000..7abb252747 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java @@ -0,0 +1,1143 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.GlideString; +import glide.api.models.commands.geospatial.GeoAddOptions; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin.CoordOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.MemberOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.MemberOriginBinary; +import glide.api.models.commands.geospatial.GeoSearchOrigin.SearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands and transactions for the "Geospatial Indices Commands" group for standalone and + * cluster clients. + * + * @see Geospatial Indices Commands + */ +public interface GeospatialIndicesBaseCommands { + + /** + * Adds geospatial members with their positions to the specified sorted set stored at key + * .
+ * If a member is already a part of the sorted set, its position is updated. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersToGeospatialData A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @param options The GeoAdd options - see {@link GeoAddOptions} + * @return The number of elements added to the sorted set. If changed is set to + * true in the options, returns the number of elements updated in the sorted set. + * @example + *
{@code
+     * GeoAddOptions options = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS, true);
+     * Long num = client.geoadd("mySortedSet", Map.of("Palermo", new GeospatialData(13.361389, 38.115556)), options).get();
+     * assert num == 1L; // Indicates that the position of an existing member in the sorted set "mySortedSet" has been updated.
+     * }
+ */ + CompletableFuture geoadd( + String key, Map membersToGeospatialData, GeoAddOptions options); + + /** + * Adds geospatial members with their positions to the specified sorted set stored at key + * .
+ * If a member is already a part of the sorted set, its position is updated. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersToGeospatialData A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @param options The GeoAdd options - see {@link GeoAddOptions} + * @return The number of elements added to the sorted set. If changed is set to + * true in the options, returns the number of elements updated in the sorted set. + * @example + *
{@code
+     * GeoAddOptions options = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS, true);
+     * Long num = client.geoadd(gs("mySortedSet"), Map.of(gs("Palermo"), new GeospatialData(13.361389, 38.115556)), options).get();
+     * assert num == 1L; // Indicates that the position of an existing member in the sorted set gs("mySortedSet") has been updated.
+     * }
+ */ + CompletableFuture geoadd( + GlideString key, + Map membersToGeospatialData, + GeoAddOptions options); + + /** + * Adds geospatial members with their positions to the specified sorted set stored at key + * .
+ * If a member is already a part of the sorted set, its position is updated.
+ * To perform a geoadd operation while specifying optional parameters, use {@link + * #geoadd(String, Map, GeoAddOptions)}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersToGeospatialData A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @return The number of elements added to the sorted set. + * @example + *
{@code
+     * Long num = client.geoadd("mySortedSet", Map.of("Palermo", new GeospatialData(13.361389, 38.115556), "Catania", new GeospatialData(15.087269, 37.502669)).get();
+     * assert num == 2L; // Indicates that two elements have been added to the sorted set "mySortedSet".
+     * }
+ */ + CompletableFuture geoadd(String key, Map membersToGeospatialData); + + /** + * Adds geospatial members with their positions to the specified sorted set stored at key + * .
+ * If a member is already a part of the sorted set, its position is updated.
+ * To perform a geoadd operation while specifying optional parameters, use {@link + * #geoadd(String, Map, GeoAddOptions)}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersToGeospatialData A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @return The number of elements added to the sorted set. + * @example + *
{@code
+     * Long num = client.geoadd(gs("mySortedSet"), Map.of(gs("Palermo"), new GeospatialData(13.361389, 38.115556), gs("Catania"), new GeospatialData(15.087269, 37.502669)).get();
+     * assert num == 2L; // Indicates that two elements have been added to the sorted set gs("mySortedSet").
+     * }
+ */ + CompletableFuture geoadd( + GlideString key, Map membersToGeospatialData); + + /** + * Returns the positions (longitude,latitude) of all the specified members of the + * geospatial index represented by the sorted set at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members The members for which to get the positions. + * @return A 2D array which represent positions (longitude and latitude) + * corresponding to the given members. If a member does not exist, its position will be + * null. + * @example + *
{@code
+     * // When added via GEOADD, the geospatial coordinates are converted into a 52 bit geohash, so the coordinates
+     * // returned might not be exactly the same as the input values
+     * client.geoadd("mySortedSet", Map.of("Palermo", new GeospatialData(13.361389, 38.115556), "Catania", new GeospatialData(15.087269, 37.502669))).get();
+     * Double[][] result = client.geopos("mySortedSet", new String[]{"Palermo", "Catania", "NonExisting"}).get();
+     * System.out.println(Arrays.deepToString(result));
+     * }
+ */ + CompletableFuture geopos(String key, String[] members); + + /** + * Returns the positions (longitude,latitude) of all the specified members of the + * geospatial index represented by the sorted set at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members The members for which to get the positions. + * @return A 2D array which represent positions (longitude and latitude) + * corresponding to the given members. If a member does not exist, its position will be + * null. + * @example + *
{@code
+     * // When added via GEOADD, the geospatial coordinates are converted into a 52 bit geohash, so the coordinates
+     * // returned might not be exactly the same as the input values
+     * client.geoadd(gs("mySortedSet"), Map.of(gs("Palermo"), new GeospatialData(13.361389, 38.115556), gs("Catania"), new GeospatialData(15.087269, 37.502669))).get();
+     * Double[][] result = client.geopos(gs("mySortedSet", new GlideString[]{gs("Palermo"), gs("Catania"), gs("NonExisting")}).get();
+     * System.out.println(Arrays.deepToString(result));
+     * }
+ */ + CompletableFuture geopos(GlideString key, GlideString[] members); + + /** + * Returns the distance between member1 and member2 saved in the + * geospatial index stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member1 The name of the first member. + * @param member2 The name of the second member. + * @param geoUnit The unit of distance measurement - see {@link GeoUnit}. + * @return The distance between member1 and member2. If one or both + * members do not exist, or if the key does not exist, returns null. + * @example + *
{@code
+     * Double result = client.geodist("mySortedSet", "Palermo", "Catania", GeoUnit.KILOMETERS).get();
+     * System.out.println(result);
+     * }
+ */ + CompletableFuture geodist(String key, String member1, String member2, GeoUnit geoUnit); + + /** + * Returns the distance between member1 and member2 saved in the + * geospatial index stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member1 The name of the first member. + * @param member2 The name of the second member. + * @param geoUnit The unit of distance measurement - see {@link GeoUnit}. + * @return The distance between member1 and member2. If one or both + * members do not exist, or if the key does not exist, returns null. + * @example + *
{@code
+     * Double result = client.geodist(gs("mySortedSet"), gs("Palermo"), gs("Catania"), GeoUnit.KILOMETERS).get();
+     * System.out.println(result);
+     * }
+ */ + CompletableFuture geodist( + GlideString key, GlideString member1, GlideString member2, GeoUnit geoUnit); + + /** + * Returns the distance between member1 and member2 saved in the + * geospatial index stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member1 The name of the first member. + * @param member2 The name of the second member. + * @return The distance between member1 and member2. If one or both + * members do not exist, or if the key does not exist, returns null. The default + * unit is {@see GeoUnit#METERS}. + * @example + *
{@code
+     * Double result = client.geodist("mySortedSet", "Palermo", "Catania").get();
+     * System.out.println(result);
+     * }
+ */ + CompletableFuture geodist(String key, String member1, String member2); + + /** + * Returns the distance between member1 and member2 saved in the + * geospatial index stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member1 The name of the first member. + * @param member2 The name of the second member. + * @return The distance between member1 and member2. If one or both + * members do not exist, or if the key does not exist, returns null. The default + * unit is {@see GeoUnit#METERS}. + * @example + *
{@code
+     * Double result = client.geodist(gs("mySortedSet"), gs("Palermo"), gs("Catania")).get();
+     * System.out.println(result);
+     * }
+ */ + CompletableFuture geodist(GlideString key, GlideString member1, GlideString member2); + + /** + * Returns the GeoHash strings representing the positions of all the specified + * members in the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members The array of members whose GeoHash strings are to be retrieved. + * @return An array of GeoHash strings representing the positions of the specified + * members stored at key. If a member does not exist in the sorted set, a + * null value is returned for that member. + * @example + *
{@code
+     * String[] result = client.geohash("mySortedSet", new String[] {"Palermo", "Catania", "NonExisting"}).get();
+     * System.out.println(Arrays.toString(result)); // prints a list of corresponding GeoHash String values
+     * }
+ */ + CompletableFuture geohash(String key, String[] members); + + /** + * Returns the GeoHash strings representing the positions of all the specified + * members in the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members The array of members whose GeoHash strings are to be retrieved. + * @return An array of GeoHash strings representing the positions of the specified + * members stored at key. If a member does not exist in the sorted set, a + * null value is returned for that member. + * @example + *
{@code
+     * GlideString[] result = client.geohash(gs("mySortedSet"), new GlideString[] {gs("Palermo"), gs("Catania"), gs("NonExisting")}).get();
+     * System.out.println(Arrays.toString(result)); // prints a list of corresponding GeoHash String values
+     * }
+ */ + CompletableFuture geohash(GlideString key, GlideString[] members); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @return An array of matched member names. + * @example + *
{@code
+     * String[] expected = new String[] {"Catania", "Palermo"};
+     * String[] result =
+     *                client
+     *                        .geosearch(
+     *                                "a1",
+     *                                new GeoSearchOrigin.MemberOrigin("Palermo"),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS))
+     *                        .get();
+     * assert Arrays.equals(expected, result);
+     * }
+ */ + CompletableFuture geosearch( + String key, SearchOrigin searchFrom, GeoSearchShape searchBy); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @return An array of matched member names. + * @example + *
{@code
+     * GlideString[] expected = new GlideString[] {gs("Catania"), gs("Palermo")};
+     * GlideString[] result =
+     *                client
+     *                        .geosearch(
+     *                                gs("a1"),
+     *                                new GeoSearchOrigin.MemberOriginBinary(gs("Palermo")),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS))
+     *                        .get();
+     * assert Arrays.equals(expected, result);
+     * }
+ */ + CompletableFuture geosearch( + GlideString key, SearchOrigin searchFrom, GeoSearchShape searchBy); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return An array of matched member names. + * @example + *
{@code
+     * String[] expected = new String[] {"Catania", "Palermo"};
+     * String[] result =
+     *                client
+     *                        .geosearch(
+     *                                "a1",
+     *                                new GeoSearchOrigin("Palermo"),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS),
+     *                                SortOrder.ASC)
+     *                        .get();
+     * assert Arrays.equals(expected, result);
+     * }
+ */ + CompletableFuture geosearch( + String key, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchResultOptions resultOptions); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return An array of matched member names. + * @example + *
{@code
+     * GlideString[] expected = new GlideString[] {gs("Catania"), gs("Palermo")};
+     * GlideString[] result =
+     *                client
+     *                        .geosearch(
+     *                                gs("a1"),
+     *                                new GeoSearchOrigin.MemberOriginBinary(gs("Palermo")),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS),
+     *                                SortOrder.ASC)
+     *                        .get();
+     * assert Arrays.equals(expected, result);
+     * }
+ */ + CompletableFuture geosearch( + GlideString key, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchResultOptions resultOptions); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @return An array of arrays where each sub-array represents a single item in the following + * order: + *
    + *
  • The member (location) name. + *
  • The distance from the center as a Double, in the same unit specified for + * searchBy. + *
  • The geohash of the location as a Long. + *
  • The coordinates as a two item array of Double. + *
+ * + * @example + *
{@code
+     * Object[] expected =
+     *            new Object[] {
+     *                new Object[] {
+     *                     // name
+     *                    "Palermo",
+     *                    new Object[] {
+     *                        // distance, hash and coordinates
+     *                        0.0, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963}
+     *                    }
+     *                },
+     *                new Object[] {
+     *                    "Catania",
+     *                    new Object[] {
+     *                        166.2742, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162}
+     *                    }
+     *                }
+     *            };
+     * Object[] result =
+     *                client
+     *                        .geosearch(
+     *                                "a1",
+     *                                new GeoSearchOrigin("Palermo"),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS),
+     *                                new GeoSearchOptions.GeoSearchOptionsBuilder()
+     *                                             .withcoord()
+     *                                             .withdist()
+     *                                             .withhash()
+     *                                             .count(3)
+     *                                             .build(),
+     *                                SortOrder.ASC)
+     *                        .get();
+     * // The result contains the data in the same format as expected.
+     * }
+ */ + CompletableFuture geosearch( + String key, SearchOrigin searchFrom, GeoSearchShape searchBy, GeoSearchOptions options); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @return An array of arrays where each sub-array represents a single item in the following + * order: + *
    + *
  • The member (location) name. + *
  • The distance from the center as a Double, in the same unit specified for + * searchBy. + *
  • The geohash of the location as a Long. + *
  • The coordinates as a two item array of Double. + *
+ * + * @example + *
{@code
+     * Object[] expected =
+     *            new Object[] {
+     *                new Object[] {
+     *                     // name
+     *                    gs("Palermo"),
+     *                    new Object[] {
+     *                        // distance, hash and coordinates
+     *                        0.0, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963}
+     *                    }
+     *                },
+     *                new Object[] {
+     *                    gs("Catania"),
+     *                    new Object[] {
+     *                        166.2742, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162}
+     *                    }
+     *                }
+     *            };
+     * Object[] result =
+     *                client
+     *                        .geosearch(
+     *                                gs("a1"),
+     *                                new GeoSearchOrigin.MemberOriginBinary(gs("Palermo")),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS),
+     *                                new GeoSearchOptions.GeoSearchOptionsBuilder()
+     *                                             .withcoord()
+     *                                             .withdist()
+     *                                             .withhash()
+     *                                             .count(3)
+     *                                             .build(),
+     *                                SortOrder.ASC)
+     *                        .get();
+     * // The result contains the data in the same format as expected.
+     * }
+ */ + CompletableFuture geosearch( + GlideString key, SearchOrigin searchFrom, GeoSearchShape searchBy, GeoSearchOptions options); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. See - {@link + * GeoSearchOptions} + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return An array of arrays where each sub-array represents a single item in the following + * order: + *
    + *
  • The member (location) name. + *
  • The distance from the center as a Double, in the same unit specified for + * searchBy. + *
  • The geohash of the location as a Long. + *
  • The coordinates as a two item array of Double. + *
+ * + * @example + *
{@code
+     * Object[] expected =
+     *            new Object[] {
+     *                new Object[] {
+     *                     // name
+     *                    "Palermo",
+     *                    new Object[] {
+     *                        // distance, hash and coordinates
+     *                        0.0, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963}
+     *                    }
+     *                },
+     *                new Object[] {
+     *                    "Catania",
+     *                    new Object[] {
+     *                        166.2742, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162}
+     *                    }
+     *                }
+     *            };
+     * Object[] result =
+     *                client
+     *                        .geosearch(
+     *                                "a1",
+     *                                new GeoSearchOrigin("Palermo"),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS),
+     *                                new GeoSearchOptions.GeoSearchOptionsBuilder()
+     *                                             .withcoord()
+     *                                             .withdist()
+     *                                             .withhash()
+     *                                             .count(3)
+     *                                             .build(),
+     *                                SortOrder.ASC)
+     *                        .get();
+     * // The result contains the data in the same format as expected.
+     * }
+ */ + CompletableFuture geosearch( + String key, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchOptions options, + GeoSearchResultOptions resultOptions); + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(String, Map)}, which are within the borders of the area specified by a given shape. + * + * @since Valkey 6.2.0 and above. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. See - {@link + * GeoSearchOptions} + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return An array of arrays where each sub-array represents a single item in the following + * order: + *
    + *
  • The member (location) name. + *
  • The distance from the center as a Double, in the same unit specified for + * searchBy. + *
  • The geohash of the location as a Long. + *
  • The coordinates as a two item array of Double. + *
+ * + * @example + *
{@code
+     * Object[] expected =
+     *            new Object[] {
+     *                new Object[] {
+     *                     // name
+     *                    gs("Palermo"),
+     *                    new Object[] {
+     *                        // distance, hash and coordinates
+     *                        0.0, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963}
+     *                    }
+     *                },
+     *                new Object[] {
+     *                    gs("Catania"),
+     *                    new Object[] {
+     *                        166.2742, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162}
+     *                    }
+     *                }
+     *            };
+     * Object[] result =
+     *                client
+     *                        .geosearch(
+     *                                gs("a1"),
+     *                                new GeoSearchOrigin.MemberOriginBinary(gs("Palermo")),
+     *                                new GeoSearchShape(200, GeoUnit.KILOMETERS),
+     *                                new GeoSearchOptions.GeoSearchOptionsBuilder()
+     *                                             .withcoord()
+     *                                             .withdist()
+     *                                             .withhash()
+     *                                             .count(3)
+     *                                             .build(),
+     *                                SortOrder.ASC)
+     *                        .get();
+     * // The result contains the data in the same format as expected.
+     * }
+ */ + CompletableFuture geosearch( + GlideString key, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchOptions options, + GeoSearchResultOptions resultOptions); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS))
+     *                     .get();
+     * assert result == 4L;
+     * }
+ */ + CompletableFuture geosearchstore( + String destination, String source, SearchOrigin searchFrom, GeoSearchShape searchBy); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS))
+     *                     .get();
+     * assert result == 4L;
+     * }
+ */ + CompletableFuture geosearchstore( + GlideString destination, + GlideString source, + SearchOrigin searchFrom, + GeoSearchShape searchBy); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape, GeoSearchResultOptions)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS),
+     *                             new GeoSearchResultOptions(2, true))
+     *                     .get();
+     * assert result == 2L;
+     * }
+ */ + CompletableFuture geosearchstore( + String destination, + String source, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchResultOptions resultOptions); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape, GeoSearchResultOptions)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS),
+     *                             new GeoSearchResultOptions(2, true))
+     *                     .get();
+     * assert result == 2L;
+     * }
+ */ + CompletableFuture geosearchstore( + GlideString destination, + GlideString source, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchResultOptions resultOptions); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape, GeoSearchOptions)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS),
+     *                             GeoSearchStoreOptions.builder().storedist().build())
+     *                     .get();
+     * assert result == 4L;
+     * }
+ */ + CompletableFuture geosearchstore( + String destination, + String source, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchStoreOptions options); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape, GeoSearchOptions)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOriginBinary} to use the position of the given existing member in the + * sorted set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS),
+     *                             GeoSearchStoreOptions.builder().storedist().build())
+     *                     .get();
+     * assert result == 4L;
+     * }
+ */ + CompletableFuture geosearchstore( + GlideString destination, + GlideString source, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchStoreOptions options); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape, GeoSearchOptions, GeoSearchResultOptions)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS),
+     *                             GeoSearchStoreOptions.builder().storedist().build()
+     *                             new GeoSearchResultOptions(2, true))
+     *                     .get();
+     * assert result == 2L;
+     * }
+ */ + CompletableFuture geosearchstore( + String destination, + String source, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchStoreOptions options, + GeoSearchResultOptions resultOptions); + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(String, SearchOrigin, + * GeoSearchShape, GeoSearchOptions, GeoSearchResultOptions)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long result = client
+     *                     .geosearchstore(
+     *                             destinationKey,
+     *                             sourceKey,
+     *                             new CoordOrigin(new GeospatialData(15, 37)),
+     *                             new GeoSearchShape(400, 400, GeoUnit.KILOMETERS),
+     *                             GeoSearchStoreOptions.builder().storedist().build()
+     *                             new GeoSearchResultOptions(2, true))
+     *                     .get();
+     * assert result == 2L;
+     * }
+ */ + CompletableFuture geosearchstore( + GlideString destination, + GlideString source, + SearchOrigin searchFrom, + GeoSearchShape searchBy, + GeoSearchStoreOptions options, + GeoSearchResultOptions resultOptions); +} diff --git a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java index 5f4506cd24..5bea78fed9 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -1,6 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.HScanOptionsBinary; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -8,14 +11,16 @@ * Supports commands and transactions for the "Hash Commands" group for standalone and cluster * clients. * - * @see Hash Commands + * @see Hash Commands */ public interface HashBaseCommands { + /** Valkey API keyword used to query hash members with their values. */ + String WITH_VALUES_VALKEY_API = "WITHVALUES"; /** * Retrieves the value associated with field in the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field in the hash stored at key to retrieve from the database. * @return The value associated with field, or null when field @@ -31,10 +36,29 @@ public interface HashBaseCommands { */ CompletableFuture hget(String key, String field); + /** + * Retrieves the value associated with field in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to retrieve from the database. + * @return The value associated with field, or null when field + * is not present in the hash or key does not exist. + * @example + *
{@code
+     * String payload = client.hget(gs("my_hash"), gs("field1")).get();
+     * assert payload.equals(gs("value"));
+     *
+     * String payload = client.hget(gs("my_hash"), gs("nonexistent_field")).get();
+     * assert payload.equals(null);
+     * }
+ */ + CompletableFuture hget(GlideString key, GlideString field); + /** * Sets the specified fields to their respective values in the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param fieldValueMap A field-value map consisting of fields and their corresponding values to * be set in the hash stored at the specified key. @@ -47,13 +71,29 @@ public interface HashBaseCommands { */ CompletableFuture hset(String key, Map fieldValueMap); + /** + * Sets the specified fields to their respective values in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param fieldValueMap A field-value map consisting of fields and their corresponding values to + * be set in the hash stored at the specified key. + * @return The number of fields that were added. + * @example + *
{@code
+     * Long num = client.hset(gs("my_hash"), Map.of(gs("field"), gs("value"), gs("field2"), gs("value2"))).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture hset(GlideString key, Map fieldValueMap); + /** * Sets field in the hash stored at key to value, only if * field does not yet exist.
* If key does not exist, a new key holding a hash is created.
* If field already exists, this operation has no effect. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field to set the value for. * @param value The value to set. @@ -70,11 +110,34 @@ public interface HashBaseCommands { */ CompletableFuture hsetnx(String key, String field, String value); + /** + * Sets field in the hash stored at key to value, only if + * field does not yet exist.
+ * If key does not exist, a new key holding a hash is created.
+ * If field already exists, this operation has no effect. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field to set the value for. + * @param value The value to set. + * @return true if the field was set, false if the field already existed + * and was not set. + * @example + *
{@code
+     * Boolean payload1 = client.hsetnx(gs("myHash"), gs("field"), gs("value")).get();
+     * assert payload1; // Indicates that the field "field" was set successfully in the hash "myHash".
+     *
+     * Boolean payload2 = client.hsetnx(gs("myHash"), gs("field"), gs("newValue")).get();
+     * assert !payload2; // Indicates that the field "field" already existed in the hash "myHash" and was not set again.
+     * }
+ */ + CompletableFuture hsetnx(GlideString key, GlideString field, GlideString value); + /** * Removes the specified fields from the hash stored at key. Specified fields that do * not exist within this hash are ignored. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param fields The fields to remove from the hash stored at key. * @return The number of fields that were removed from the hash, not including specified but @@ -88,10 +151,28 @@ public interface HashBaseCommands { */ CompletableFuture hdel(String key, String[] fields); + /** + * Removes the specified fields from the hash stored at key. Specified fields that do + * not exist within this hash are ignored. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param fields The fields to remove from the hash stored at key. + * @return The number of fields that were removed from the hash, not including specified but + * non-existing fields.
+ * If key does not exist, it is treated as an empty hash and it returns 0.
+ * @example + *
{@code
+     * Long num = client.hdel("my_hash", new String[] {gs("field1"), gs("field2")}).get();
+     * assert num == 2L; //Indicates that two fields were successfully removed from the hash.
+     * }
+ */ + CompletableFuture hdel(GlideString key, GlideString[] fields); + /** * Returns the number of fields contained in the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @return The number of fields in the hash, or 0 when the key does not exist.
* If key holds a value that is not a hash, an error is returned. @@ -106,25 +187,58 @@ public interface HashBaseCommands { */ CompletableFuture hlen(String key); + /** + * Returns the number of fields contained in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @return The number of fields in the hash, or 0 when the key does not exist.
+ * If key holds a value that is not a hash, an error is returned. + * @example + *
{@code
+     * Long num1 = client.hlen(gs("myHash")).get();
+     * assert num1 == 3L;
+     *
+     * Long num2 = client.hlen(gs("nonExistingKey")).get();
+     * assert num2 == 0L;
+     * }
+ */ + CompletableFuture hlen(GlideString key); + /** * Returns all values in the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @return An array of values in the hash, or an empty array when the * key does not exist. * @example *
{@code
      * String[] values = client.hvals("myHash").get();
-     * assert values.equals(new String[] {"value1", "value2", "value3"}); // Returns all the values stored in the hash "myHash".
+     * assert Arrays.equals(values, new String[] {"value1", "value2", "value3"}); // Returns all the values stored in the hash "myHash".
      * }
*/ CompletableFuture hvals(String key); + /** + * Returns all values in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @return An array of values in the hash, or an empty array when the + * key does not exist. + * @example + *
{@code
+     * GlideString[] values = client.hvals(gs("myHash")).get();
+     * assert Arrays.equals(values, new GlideString[] {gs("value1"), gs("value2"), gs("value3")}); // Returns all the values stored in the hash "myHash".
+     * }
+ */ + CompletableFuture hvals(GlideString key); + /** * Returns the values associated with the specified fields in the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param fields The fields in the hash stored at key to retrieve from the database. * @return An array of values associated with the given fields, in the same order as they are @@ -135,15 +249,34 @@ public interface HashBaseCommands { * @example *
{@code
      * String[] values = client.hmget("my_hash", new String[] {"field1", "field2"}).get()
-     * assert values.equals(new String[] {"value1", "value2"});
+     * assert Arrays.equals(values, new String[] {"value1", "value2"});
      * }
*/ CompletableFuture hmget(String key, String[] fields); + /** + * Returns the values associated with the specified fields in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param fields The fields in the hash stored at key to retrieve from the database. + * @return An array of values associated with the given fields, in the same order as they are + * requested.
+ * For every field that does not exist in the hash, a null value is returned.
+ * If key does not exist, it is treated as an empty hash, and it returns an array + * of null values.
+ * @example + *
{@code
+     * GlideString[] values = client.hmget(gs("my_hash"), new GlideString[] {gs("field1"), gs("field2")}).get()
+     * assert Arrays.equals(values, new GlideString[] {gs("value1"), gs("value2")});
+     * }
+ */ + CompletableFuture hmget(GlideString key, GlideString[] fields); + /** * Returns if field is an existing field in the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field to check in the hash stored at key. * @return True if the hash contains the specified field. If the hash does not @@ -159,10 +292,29 @@ public interface HashBaseCommands { */ CompletableFuture hexists(String key, String field); + /** + * Returns if field is an existing field in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field to check in the hash stored at key. + * @return True if the hash contains the specified field. If the hash does not + * contain the field, or if the key does not exist, it returns False. + * @example + *
{@code
+     * Boolean exists = client.hexists(gs("my_hash"), gs("field1")).get();
+     * assert exists;
+     *
+     * Boolean exists = client.hexists(gs("my_hash"), gs("non_existent_field")).get();
+     * assert !exists;
+     * }
+ */ + CompletableFuture hexists(GlideString key, GlideString field); + /** * Returns all fields and values of the hash stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @return A Map of fields and their values stored in the hash. Every field name in * the map is associated with its corresponding value.
@@ -175,13 +327,29 @@ public interface HashBaseCommands { */ CompletableFuture> hgetall(String key); + /** + * Returns all fields and values of the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @return A Map of fields and their values stored in the hash. Every field name in + * the map is associated with its corresponding value.
+ * If key does not exist, it returns an empty map. + * @example + *
{@code
+     * Map fieldValueMap = client.hgetall(gs("my_hash")).get();
+     * assert fieldValueMap.equals(Map.of(gs("field1"), gs("value1"), gs("field2"), gs("value2")));
+     * }
+ */ + CompletableFuture> hgetall(GlideString key); + /** * Increments the number stored at field in the hash stored at key by * increment. By using a negative increment value, the value stored at field in the * hash stored at key is decremented. If field or key does * not exist, it is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field in the hash stored at key to increment or decrement its * value. @@ -197,6 +365,28 @@ public interface HashBaseCommands { */ CompletableFuture hincrBy(String key, String field, long amount); + /** + * Increments the number stored at field in the hash stored at key by + * increment. By using a negative increment value, the value stored at field in the + * hash stored at key is decremented. If field or key does + * not exist, it is set to 0 before performing the operation. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to increment or decrement its + * value. + * @param amount The amount by which to increment or decrement the field's value. Use a negative + * value to decrement. + * @return The value of field in the hash stored at key after the + * increment or decrement. + * @example + *
{@code
+     * Long num = client.hincrBy(gs("my_hash"), gs("field1"), 5).get();
+     * assert num == 5L;
+     * }
+ */ + CompletableFuture hincrBy(GlideString key, GlideString field, long amount); + /** * Increments the string representing a floating point number stored at field in the * hash stored at key by increment. By using a negative increment value, the value @@ -204,7 +394,7 @@ public interface HashBaseCommands { * field
or key does not exist, it is set to 0 before performing the * operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field in the hash stored at key to increment or decrement its * value. @@ -219,4 +409,350 @@ public interface HashBaseCommands { * } */ CompletableFuture hincrByFloat(String key, String field, double amount); + + /** + * Increments the string representing a floating point number stored at field in the + * hash stored at key by increment. By using a negative increment value, the value + * stored at field in the hash stored at key is decremented. If + * field or key does not exist, it is set to 0 before performing the + * operation. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to increment or decrement its + * value. + * @param amount The amount by which to increment or decrement the field's value. Use a negative + * value to decrement. + * @return The value of field in the hash stored at key after the + * increment or decrement. + * @example + *
{@code
+     * Double num = client.hincrByFloat(gs("my_hash"), gs("field1"), 2.5).get();
+     * assert num == 2.5;
+     * }
+ */ + CompletableFuture hincrByFloat(GlideString key, GlideString field, double amount); + + /** + * Returns all field names in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @return An array of field names in the hash, or an empty array when + * the key does not exist. + * @example + *
{@code
+     * String[] names = client.hkeys("my_hash").get();
+     * assert Arrays.equals(names, new String[] { "field_1", "field_2" });
+     * }
+ */ + CompletableFuture hkeys(String key); + + /** + * Returns all field names in the hash stored at key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @return An array of field names in the hash, or an empty array when + * the key does not exist. + * @example + *
{@code
+     * GlideString[] names = client.hkeys(gs("my_hash")).get();
+     * assert Arrays.equals(names, new GlideString[] { gs("field_1"), gs("field_2") });
+     * }
+ */ + CompletableFuture hkeys(GlideString key); + + /** + * Returns the string length of the value associated with field in the hash stored at + * key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash. + * @return The string length or 0 if field or key does not + * exist. + * @example + *
{@code
+     * Long strlen = client.hstrlen("my_hash", "my_field").get();
+     * assert strlen >= 0L;
+     * }
+ */ + CompletableFuture hstrlen(String key, String field); + + /** + * Returns the string length of the value associated with field in the hash stored at + * key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash. + * @return The string length or 0 if field or key does not + * exist. + * @example + *
{@code
+     * Long strlen = client.hstrlen(gs("my_hash"), gs("my_field")).get();
+     * assert strlen >= 0L;
+     * }
+ */ + CompletableFuture hstrlen(GlideString key, GlideString field); + + /** + * Returns a random field name from the hash value stored at key. + * + * @since Valkey 6.2 and above. + * @see valkey.io for details. + * @param key The key of the hash. + * @return A random field name from the hash stored at key, or null when + * the key does not exist. + * @example + *
{@code
+     * String field = client.hrandfield("my_hash").get();
+     * System.out.printf("A random field from the hash is '%s'", field);
+     * }
+ */ + CompletableFuture hrandfield(String key); + + /** + * Returns a random field name from the hash value stored at key. + * + * @since Valkey 6.2 and above. + * @see valkey.io for details. + * @param key The key of the hash. + * @return A random field name from the hash stored at key, or null when + * the key does not exist. + * @example + *
{@code
+     * GlideString field = client.hrandfield(gs("my_hash")).get();
+     * System.out.printf("A random field from the hash is '%s'", field);
+     * }
+ */ + CompletableFuture hrandfield(GlideString key); + + /** + * Retrieves up to count random field names from the hash value stored at key + * . + * + * @since Valkey 6.2 and above. + * @see valkey.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return An array of random field names from the hash stored at key, + * or an empty array when the key does not exist. + * @example + *
{@code
+     * String[] fields = client.hrandfieldWithCount("my_hash", 10).get();
+     * System.out.printf("Random fields from the hash are '%s'", String.join(", ", fields));
+     * }
+ */ + CompletableFuture hrandfieldWithCount(String key, long count); + + /** + * Retrieves up to count random field names from the hash value stored at key + * . + * + * @since Valkey 6.2 and above. + * @see valkey.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return An array of random field names from the hash stored at key, + * or an empty array when the key does not exist. + * @example + *
{@code
+     * GlideString[] fields = client.hrandfieldWithCount(gs("my_hash"), 10).get();
+     * System.out.printf("Random fields from the hash are '%s'", GlideString.join(", ", fields));
+     * }
+ */ + CompletableFuture hrandfieldWithCount(GlideString key, long count); + + /** + * Retrieves up to count random field names along with their values from the hash + * value stored at key. + * + * @since Valkey 6.2 and above. + * @see valkey.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return A 2D array of [fieldName, value] arrays, where + * fieldName is a random field name from the hash and value is the + * associated value of the field name.
+ * If the hash does not exist or is empty, the response will be an empty array. + * @example + *
{@code
+     * String[][] fields = client.hrandfieldWithCountWithValues("my_hash", 1).get();
+     * System.out.printf("A random field from the hash is '%s' and the value is '%s'", fields[0][0], fields[0][1]);
+     * }
+ */ + CompletableFuture hrandfieldWithCountWithValues(String key, long count); + + /* + * Retrieves up to count random field names along with their values from the hash + * value stored at key. + * + * @since Valkey 6.2 and above. + * @see valkey.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return A 2D array of [fieldName, value] arrays, where + * fieldName is a random field name from the hash and value is the + * associated value of the field name.
+ * If the hash does not exist or is empty, the response will be an empty array. + * @example + *
{@code
+     * GlideString[][] fields = client.hrandfieldWithCountWithValues(gs("my_hash"), 1).get();
+     * System.out.printf("A random field from the hash is '%s' and the value is '%s'", fields[0][0], fields[0][1]);
+     * }
+ */ + CompletableFuture hrandfieldWithCountWithValues(GlideString key, long count); + + /** + * Iterates fields of Hash types and their associated values. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the result. The second element is always an + * Array of the subset of the hash held in key. The array in the + * second element is always a flattened series of String pairs, where the key is + * at even indices and the value is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *   result = client.hscan(key1, cursor).get();
+     *   cursor = result[0].toString();
+     *   Object[] stringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nHSCAN iteration:");
+     *   for (int i = 0; i < stringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", stringResults[i], stringResults[i + 1]);
+     *     if (i + 2 < stringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture hscan(String key, String cursor); + + /** + * Iterates fields of Hash types and their associated values. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the result. The second element is always an + * Array of the subset of the hash held in key. The array in the + * second element is always a flattened series of String pairs, where the key is + * at even indices and the value is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *   result = client.hscan(key1, cursor).get();
+     *   cursor = gs(result[0].toString());
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nHSCAN iteration:");
+     *   for (int i = 0; i < glideStringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", glideStringResults[i], glideStringResults[i + 1]);
+     *     if (i + 2 < glideStringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture hscan(GlideString key, GlideString cursor); + + /** + * Iterates fields of Hash types and their associated values. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param hScanOptions The {@link HScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the result. The second element is always an + * Array of the subset of the hash held in key. The array in the + * second element is always a flattened series of String pairs, where the key is + * at even indices and the value is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *   result = client.hscan(key1, cursor, HScanOptions.builder().matchPattern("*").count(20L).build()).get();
+     *   cursor = result[0].toString();
+     *   Object[] stringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nHSCAN iteration:");
+     *   for (int i = 0; i < stringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", stringResults[i], stringResults[i + 1]);
+     *     if (i + 2 < stringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture hscan(String key, String cursor, HScanOptions hScanOptions); + + /** + * Iterates fields of Hash types and their associated values. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param hScanOptions The {@link HScanOptionsBinary}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the result. The second element is always an + * Array of the subset of the hash held in key. The array in the + * second element is always a flattened series of String pairs, where the key is + * at even indices and the value is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *   result = client.hscan(key1, cursor, HScanOptionsBinary.builder().matchPattern(gs("*")).count(20L).build()).get();
+     *   cursor = gs(result[0].toString());
+     *   Object[] gslideStringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nHSCAN iteration:");
+     *   for (int i = 0; i < gslideStringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", gslideStringResults[i], gslideStringResults[i + 1]);
+     *     if (i + 2 < gslideStringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture hscan( + GlideString key, GlideString cursor, HScanOptionsBinary hScanOptions); } diff --git a/java/client/src/main/java/glide/api/commands/HyperLogLogBaseCommands.java b/java/client/src/main/java/glide/api/commands/HyperLogLogBaseCommands.java index 97e4cd4cbf..886a9318db 100644 --- a/java/client/src/main/java/glide/api/commands/HyperLogLogBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HyperLogLogBaseCommands.java @@ -1,13 +1,14 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import java.util.concurrent.CompletableFuture; /** * Supports commands and transactions for the "HyperLogLog Commands" group for standalone and * cluster clients. * - * @see HyperLogLog Commands + * @see HyperLogLog Commands */ public interface HyperLogLogBaseCommands { @@ -20,7 +21,7 @@ public interface HyperLogLogBaseCommands { * HyperLogLog, then no operation is performed. If key does not exist, then the * HyperLogLog structure is created. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the HyperLogLog data structure to add elements into. * @param elements An array of members to add to the HyperLogLog stored at key. * @return If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is @@ -36,11 +37,37 @@ public interface HyperLogLogBaseCommands { */ CompletableFuture pfadd(String key, String[] elements); + /** + * Adds all elements to the HyperLogLog data structure stored at the specified key. + *
+ * Creates a new structure if the key does not exist. + * + *

When no elements are provided, and key exists and is a + * HyperLogLog, then no operation is performed. If key does not exist, then the + * HyperLogLog structure is created. + * + * @see valkey.io for details. + * @param key The key of the HyperLogLog data structure to add elements into. + * @param elements An array of members to add to the HyperLogLog stored at key. + * @return If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is + * altered, then returns 1. Otherwise, returns 0. + * @example + *

{@code
+     * Long result = client.pfadd(gs("hll_1"), new GlideString[] { gs("a"), gs("b"), gs("c") }).get();
+     * assert result == 1L; // A data structure was created or modified
+     *
+     * result = client.pfadd(gs("hll_2"), new GlideString[0]).get();
+     * assert result == 1L; // A new empty data structure was created
+     * }
+ */ + CompletableFuture pfadd(GlideString key, GlideString[] elements); + /** * Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. * - * @see redis.io for details. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. * @param keys The keys of the HyperLogLog data structures to be analyzed. * @return The approximated cardinality of given HyperLogLog data structures.
* The cardinality of a key that does not exist is 0. @@ -52,12 +79,31 @@ public interface HyperLogLogBaseCommands { */ CompletableFuture pfcount(String[] keys); + /** + * Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or + * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the HyperLogLog data structures to be analyzed. + * @return The approximated cardinality of given HyperLogLog data structures.
+ * The cardinality of a key that does not exist is 0. + * @example + *
{@code
+     * Long result = client.pfcount(gs("hll_1"), gs("hll_2")).get();
+     * assert result == 42L; // Count of unique elements in multiple data structures
+     * }
+ */ + CompletableFuture pfcount(GlideString[] keys); + /** * Merges multiple HyperLogLog values into a unique value.
* If the destination variable exists, it is treated as one of the source HyperLogLog data sets, * otherwise a new HyperLogLog is created. * - * @see redis.io for details. + * @apiNote When in cluster mode, destination and all keys in sourceKeys + * must map to the same hash slot. + * @see valkey.io for details. * @param destination The key of the destination HyperLogLog where the merged data sets will be * stored. * @param sourceKeys The keys of the HyperLogLog structures to be merged. @@ -72,4 +118,27 @@ public interface HyperLogLogBaseCommands { * } */ CompletableFuture pfmerge(String destination, String[] sourceKeys); + + /** + * Merges multiple HyperLogLog values into a unique value.
+ * If the destination variable exists, it is treated as one of the source HyperLogLog data sets, + * otherwise a new HyperLogLog is created. + * + * @apiNote When in cluster mode, destination and all keys in sourceKeys + * must map to the same hash slot. + * @see valkey.io for details. + * @param destination The key of the destination HyperLogLog where the merged data sets will be + * stored. + * @param sourceKeys The keys of the HyperLogLog structures to be merged. + * @return OK. + * @example + *
{@code
+     * String response = client.pfmerge(gs("new_HLL"), gs("old_HLL_1"), gs("old_HLL_2")).get();
+     * assert response.equals("OK"); // new HyperLogLog data set was created with merged content of old ones
+     *
+     * String response = client.pfmerge(gs("old_HLL_1"), gs("old_HLL_2"), gs("old_HLL_3")).get();
+     * assert response.equals("OK"); // content of existing HyperLogLogs was merged into existing variable
+     * }
+ */ + CompletableFuture pfmerge(GlideString destination, GlideString[] sourceKeys); } diff --git a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java index c85b85fb68..f7004b4539 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -1,16 +1,23 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.LInsertOptions.InsertPosition; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import lombok.NonNull; /** * Supports commands and transactions for the "List Commands" group for standalone and cluster * clients. * - * @see List Commands + * @see List Commands */ public interface ListBaseCommands { + /** Valkey API keyword used to extract specific count of members from a sorted set. */ + String COUNT_FOR_LIST_VALKEY_API = "COUNT"; /** * Inserts all the specified values at the head of the list stored at key. @@ -18,7 +25,7 @@ public interface ListBaseCommands { * element to the rightmost element. If key does not exist, it is created as an empty * list before performing the push operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param elements The elements to insert at the head of the list stored at key. * @return The length of the list after the push operation. @@ -33,11 +40,32 @@ public interface ListBaseCommands { */ CompletableFuture lpush(String key, String[] elements); + /** + * Inserts all the specified values at the head of the list stored at key. + * elements are inserted one after the other to the head of the list, from the leftmost + * element to the rightmost element. If key does not exist, it is created as an empty + * list before performing the push operation. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long pushCount1 = client.lpush(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert pushCount1 == 2L;
+     *
+     * Long pushCount2 = client.lpush(gs("nonexistent_list"), new GlideString[] {gs("new_value")}).get();
+     * assert pushCount2 == 1L;
+     * }
+ */ + CompletableFuture lpush(GlideString key, GlideString[] elements); + /** * Removes and returns the first elements of the list stored at key. The command pops * a single element from the beginning of the list. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @return The value of the first element.
* If key does not exist, null will be returned. @@ -52,11 +80,224 @@ public interface ListBaseCommands { */ CompletableFuture lpop(String key); + /** + * Removes and returns the first elements of the list stored at key. The command pops + * a single element from the beginning of the list. + * + * @see valkey.io for details. + * @param key The key of the list. + * @return The value of the first element.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * GlideString value1 = client.lpop(gs("my_list")).get();
+     * assert value1.equals(gs("value1"));
+     *
+     * GlideString value2 = client.lpop(gs("non_exiting_key")).get();
+     * assert value2.equals(null);
+     * }
+ */ + CompletableFuture lpop(GlideString key); + + /** + * Returns the index of the first occurrence of element inside the list specified by + * key. If no match is found, null is returned. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @return The index of the first occurrence of element, or null if + * element is not in the list. + * @example + *
{@code
+     * Long listLen = client.rpush("my_list", new String[] {"a", "b", "c", "d", "e", "e"}).get();
+     * Long position = client.lpos("my_list", "e").get();
+     * assert position == 4L;
+     * }
+ */ + CompletableFuture lpos(String key, String element); + + /** + * Returns the index of the first occurrence of element inside the list specified by + * key. If no match is found, null is returned. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @return The index of the first occurrence of element, or null if + * element is not in the list. + * @example + *
{@code
+     * Long listLen = client.rpush(gs("my_list"), new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e"), gs("e")}).get();
+     * Long position = client.lpos(gs("my_list"), gs("e")).get();
+     * assert position == 4L;
+     * }
+ */ + CompletableFuture lpos(GlideString key, GlideString element); + + /** + * Returns the index of an occurrence of element within a list based on the given + * options. If no match is found, null is returned. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param options The LPos options. + * @return The index of element, or null if element is not + * in the list. + * @example + *
{@code
+     * Long listLen = client.rpush("my_list", new String[] {"a", "b", "c", "d", "e", "e"}).get();
+     *
+     * // Returns the second occurrence of the element "e".
+     * LPosOptions options1 = LPosOptions.builder().rank(2L).build();
+     * Long position1 = client.lpos("my_list", "e", options1).get();
+     * assert position1 == 5L;
+     *
+     * // rank and maxLength
+     * LPosOptions options2 = LPosOptions.builder().rank(1L).maxLength(1000L).build();
+     * Long position2 = client.lpos("my_list", "e", options2).get();
+     * assert position2 == 4L;
+     * }
+ */ + CompletableFuture lpos( + @NonNull String key, @NonNull String element, @NonNull LPosOptions options); + + /** + * Returns the index of an occurrence of element within a list based on the given + * options. If no match is found, null is returned. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param options The LPos options. + * @return The index of element, or null if element is not + * in the list. + * @example + *
{@code
+     * Long listLen = client.rpush(gs("my_list")), new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e"), gs("e")}).get();
+     *
+     * // Returns the second occurrence of the element gs("e").
+     * LPosOptions options1 = LPosOptions.builder().rank(2L).build();
+     * Long position1 = client.lpos(gs("my_list"), gs("e"), options1).get();
+     * assert position1 == 5L;
+     *
+     * // rank and maxLength
+     * LPosOptions options2 = LPosOptions.builder().rank(1L).maxLength(1000L).build();
+     * Long position2 = client.lpos(gs("my_list"), gs("e"), options2).get();
+     * assert position2 == 4L;
+     * }
+ */ + CompletableFuture lpos( + @NonNull GlideString key, @NonNull GlideString element, @NonNull LPosOptions options); + + /** + * Returns an array of indices of matching elements within a list. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param count The number of matches wanted. + * @return An array that holds the indices of the matching elements within the list. + * @example + *
{@code
+     * Long listLen = client.rpush("my_list", new String[] {"a", "b", "c", "d", "e", "e", "e"}).get();
+     * Long[] position = client.lposCount("my_list", "e", 3L).get());
+     * assertArrayEquals(new Long[]{4L, 5L, 6L}, position);
+     * }
+ */ + CompletableFuture lposCount(@NonNull String key, @NonNull String element, long count); + + /** + * Returns an array of indices of matching elements within a list. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param count The number of matches wanted. + * @return An array that holds the indices of the matching elements within the list. + * @example + *
{@code
+     * Long listLen = client.rpush(gs("my_list"), new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e"), gs("e"), gs("e")}).get();
+     * Long[] position = client.lposCount(gs("my_list"), gs("e"), 3L).get());
+     * assertArrayEquals(new Long[]{4L, 5L, 6L}, position);
+     * }
+ */ + CompletableFuture lposCount( + @NonNull GlideString key, @NonNull GlideString element, long count); + + /** + * Returns an array of indices of matching elements within a list based on the given + * options. If no match is found, an empty arrayis returned. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param count The number of matches wanted. + * @param options The LPos options. + * @return An array that holds the indices of the matching elements within the list. + * @example + *
{@code
+     * Long listLen = client.rpush("my_list", new String[] {"a", "b", "c", "d", "e", "e", "e"}).get();
+     *
+     * // rank
+     * LPosOptions options1 = LPosOptions.builder().rank(2L).build();
+     * Long[] position1 = client.lposCount("my_list", "e", 1L, options1).get();
+     * assertArrayEquals(new Long[]{5L}, position1);
+     *
+     * // rank and maxLength
+     * LPosOptions options2 = LPosOptions.builder.rank(2L).maxLength(1000L).build();
+     * Long[] position2 = client.lposCount("my_list", "e", 3L, options2).get();
+     * assertArrayEquals(new Long[]{5L, 6L}, position2);
+     * }
+ */ + CompletableFuture lposCount( + @NonNull String key, @NonNull String element, long count, @NonNull LPosOptions options); + + /** + * Returns an array of indices of matching elements within a list based on the given + * options. If no match is found, an empty arrayis returned. + * + * @since Valkey 6.0.6. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param count The number of matches wanted. + * @param options The LPos options. + * @return An array that holds the indices of the matching elements within the list. + * @example + *
{@code
+     * Long listLen = client.rpush(gs("my_list"), new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e"), gs("e"), gs("e")}).get();
+     *
+     * // rank
+     * LPosOptions options1 = LPosOptions.builder().rank(2L).build();
+     * Long[] position1 = client.lposCount(gs("my_list"), gs("e"), 1L, options1).get();
+     * assertArrayEquals(new Long[]{5L}, position1);
+     *
+     * // rank and maxLength
+     * LPosOptions options2 = LPosOptions.builder.rank(2L).maxLength(1000L).build();
+     * Long[] position2 = client.lposCount(gs("my_list"), gs("e"), 3L, options2).get();
+     * assertArrayEquals(new Long[]{5L, 6L}, position2);
+     * }
+ */ + CompletableFuture lposCount( + @NonNull GlideString key, + @NonNull GlideString element, + long count, + @NonNull LPosOptions options); + /** * Removes and returns up to count elements of the list stored at key, * depending on the list's length. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param count The count of the elements to pop from the list. * @return An array of the popped elements will be returned depending on the list's length.
@@ -72,6 +313,26 @@ public interface ListBaseCommands { */ CompletableFuture lpopCount(String key, long count); + /** + * Removes and returns up to count elements of the list stored at key, + * depending on the list's length. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param count The count of the elements to pop from the list. + * @return An array of the popped elements will be returned depending on the list's length.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * GlideString[] values1 = client.lpopCount(gs("my_list"), 2).get();
+     * assert values1.equals(new GlideString[] {gs("value1"), gs("value2")});
+     *
+     * GlideString[] values2 = client.lpopCount(gs("non_exiting_key") , 7).get();
+     * assert values2.equals(null);
+     * }
+ */ + CompletableFuture lpopCount(GlideString key, long count); + /** * Returns the specified elements of the list stored at key.
* The offsets start and end are zero-based indexes, with 0 @@ -80,7 +341,7 @@ public interface ListBaseCommands { * -1 being the last element of the list, -2 being the penultimate, and * so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param start The starting point of the range. * @param end The end of the range. @@ -104,6 +365,38 @@ public interface ListBaseCommands { */ CompletableFuture lrange(String key, long start, long end); + /** + * Returns the specified elements of the list stored at key.
+ * The offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Array of elements in the specified range.
+ * If start exceeds the end of the list, or if start is greater than + * end, an empty array will be returned.
+ * If end exceeds the actual end of the list, the range will stop at the actual + * end of the list.
+ * If key does not exist an empty array will be returned. + * @example + *
{@code
+     * GlideString[] payload = lient.lrange(gs("my_list"), 0, 2).get();
+     * assert Arrays.equals(new GlideString[] {gs("value1"), gs("value2"), gs("value3")});
+     *
+     * GlideString[] payload = client.lrange(gs("my_list"), -2, -1).get();
+     * assert Arrays.equals(new GlideString[] {gs("value2"), gs("value3")});
+     *
+     * GlideString[] payload = client.lrange(gs("non_exiting_key"), 0, 2).get();
+     * assert Arrays.equals(new GlideString[] {});
+     * }
+ */ + CompletableFuture lrange(GlideString key, long start, long end); + /** * Returns the element at index from the list stored at key.
* The index is zero-based, so 0 means the first element, 1 the second @@ -111,7 +404,7 @@ public interface ListBaseCommands { * the list. Here, -1 means the last element, -2 means the penultimate * and so forth. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param index The index of the element in the list to retrieve. * @return The element at index in the list stored at key.
@@ -120,14 +413,38 @@ public interface ListBaseCommands { * @example *
{@code
      * String payload1 = client.lindex("myList", 0).get();
-     * assert payload1.equals('value1'); // Returns the first element in the list stored at 'myList'.
+     * assert payload1.equals("value1"); // Returns the first element in the list stored at 'myList'.
      *
      * String payload2 = client.lindex("myList", -1).get();
-     * assert payload2.equals('value3'); // Returns the last element in the list stored at 'myList'.
+     * assert payload2.equals("value3"); // Returns the last element in the list stored at 'myList'.
      * }
*/ CompletableFuture lindex(String key, long index); + /** + * Returns the element at index from the list stored at key.
+ * The index is zero-based, so 0 means the first element, 1 the second + * element and so on. Negative indices can be used to designate elements starting at the tail of + * the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param index The index of the element in the list to retrieve. + * @return The element at index in the list stored at key.
+ * If index is out of range or if key does not exist, null + * is returned. + * @example + *
{@code
+     * String payload1 = client.lindex(gs("myList"), 0).get();
+     * assert payload1.equals(gs("value1")); // Returns the first element in the list stored at 'myList'.
+     *
+     * String payload2 = client.lindex(gs("myList"), -1).get();
+     * assert payload2.equals(gs("value3")); // Returns the last element in the list stored at 'myList'.
+     * }
+ */ + CompletableFuture lindex(GlideString key, long index); + /** * Trims an existing list so that it will contain only the specified range of elements specified. *
@@ -136,7 +453,7 @@ public interface ListBaseCommands { * These offsets can also be negative numbers indicating offsets starting at the end of the list, * with -1 being the last element of the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param start The starting point of the range. * @param end The end of the range. @@ -154,10 +471,36 @@ public interface ListBaseCommands { */ CompletableFuture ltrim(String key, long start, long end); + /** + * Trims an existing list so that it will contain only the specified range of elements specified. + *
+ * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on.
+ * These offsets can also be negative numbers indicating offsets starting at the end of the list, + * with -1 being the last element of the list, -2 being the penultimate, and so on. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Always OK.
+ * If start exceeds the end of the list, or if start is greater than + * end, the result will be an empty list (which causes key to be removed).
+ * If end exceeds the actual end of the list, it will be treated like the last + * element of the list.
+ * If key does not exist, OK will be returned without changes to the database. + * @example + *
{@code
+     * String payload = client.ltrim(gs("my_list"), 0, 1).get();
+     * assert payload.equals("OK");
+     * }
+ */ + CompletableFuture ltrim(GlideString key, long start, long end); + /** * Returns the length of the list stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @return The length of the list at key.
* If key does not exist, it is interpreted as an empty list and 0 @@ -170,6 +513,22 @@ public interface ListBaseCommands { */ CompletableFuture llen(String key); + /** + * Returns the length of the list stored at key. + * + * @see valkey.io for details. + * @param key The key of the list. + * @return The length of the list at key.
+ * If key does not exist, it is interpreted as an empty list and 0 + * is returned. + * @example + *
{@code
+     * Long lenList = client.llen(gs("my_list")).get();
+     * assert lenList == 3L //Indicates that there are 3 elements in the list.;
+     * }
+ */ + CompletableFuture llen(GlideString key); + /** * Removes the first count occurrences of elements equal to element from * the list stored at key.
@@ -180,7 +539,7 @@ public interface ListBaseCommands { * If count is 0 or count is greater than the occurrences of elements * equal to element, it removes all elements equal to element. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param count The count of the occurrences of elements equal to element to remove. * @param element The element to remove from the list. @@ -194,13 +553,37 @@ public interface ListBaseCommands { */ CompletableFuture lrem(String key, long count, String element); + /** + * Removes the first count occurrences of elements equal to element from + * the list stored at key.
+ * If count is positive: Removes elements equal to element moving from + * head to tail.
+ * If count is negative: Removes elements equal to element moving from + * tail to head.
+ * If count is 0 or count is greater than the occurrences of elements + * equal to element, it removes all elements equal to element. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param count The count of the occurrences of elements equal to element to remove. + * @param element The element to remove from the list. + * @return The number of the removed elements.
+ * If key does not exist, 0 is returned. + * @example + *
{@code
+     * Long num = client.rem(gs("my_list"), 2, gs("value")).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture lrem(GlideString key, long count, GlideString element); + /** * Inserts all the specified values at the tail of the list stored at key.
* elements are inserted one after the other to the tail of the list, from the * leftmost element to the rightmost element. If key does not exist, it is created as * an empty list before performing the push operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param elements The elements to insert at the tail of the list stored at key. * @return The length of the list after the push operation. @@ -215,11 +598,32 @@ public interface ListBaseCommands { */ CompletableFuture rpush(String key, String[] elements); + /** + * Inserts all the specified values at the tail of the list stored at key.
+ * elements are inserted one after the other to the tail of the list, from the + * leftmost element to the rightmost element. If key does not exist, it is created as + * an empty list before performing the push operation. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long pushCount1 = client.rpush(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert pushCount1 == 2L;
+     *
+     * Long pushCount2 = client.rpush(gs("nonexistent_list"), new GlideString[] {gs("new_value")}).get();
+     * assert pushCount2 == 1L;
+     * }
+ */ + CompletableFuture rpush(GlideString key, GlideString[] elements); + /** * Removes and returns the last elements of the list stored at key.
* The command pops a single element from the end of the list. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @return The value of the last element.
* If key does not exist, null will be returned. @@ -234,11 +638,30 @@ public interface ListBaseCommands { */ CompletableFuture rpop(String key); + /** + * Removes and returns the last elements of the list stored at key.
+ * The command pops a single element from the end of the list. + * + * @see valkey.io for details. + * @param key The key of the list. + * @return The value of the last element.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * GlideString value1 = client.rpop(gs("my_list")).get();
+     * assert value1.equals(gs("value1"));
+     *
+     * GlideString value2 = client.rpop(gs("non_exiting_key")).get();
+     * assert value2.equals(null);
+     * }
+ */ + CompletableFuture rpop(GlideString key); + /** * Removes and returns up to count elements from the list stored at key, * depending on the list's length. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param count The count of the elements to pop from the list. * @return An array of popped elements will be returned depending on the list's length.
@@ -254,11 +677,31 @@ public interface ListBaseCommands { */ CompletableFuture rpopCount(String key, long count); + /** + * Removes and returns up to count elements from the list stored at key, + * depending on the list's length. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param count The count of the elements to pop from the list. + * @return An array of popped elements will be returned depending on the list's length.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * GlideString[] values1 = client.rpopCount(gs("my_list"), 2).get();
+     * assert Arrays.equals(values1, new GlideString[] {gs("value1"), gs("value2")});
+     *
+     * GlideString[] values2 = client.rpopCount(gs("non_exiting_key"), 7).get();
+     * assert values2.equals(null);
+     * }
+ */ + CompletableFuture rpopCount(GlideString key, long count); + /** * Inserts element in the list at key either before or after the * pivot. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param position The relative position to insert into - either {@link InsertPosition#BEFORE} or * {@link InsertPosition#AFTER} the pivot. @@ -277,20 +720,48 @@ CompletableFuture linsert( String key, InsertPosition position, String pivot, String element); /** - * Pops an element from the head of the first list that is non-empty, with the given keys being - * checked in the order that they are given.
+ * Inserts element in the list at key either before or after the + * pivot. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param position The relative position to insert into - either {@link InsertPosition#BEFORE} or + * {@link InsertPosition#AFTER} the pivot. + * @param pivot An element of the list. + * @param element The new element to insert. + * @return The list length after a successful insert operation.
+ * If the key doesn't exist returns -1.
+ * If the pivot wasn't found, returns 0. + * @example + *
{@code
+     * Long length = client.linsert(gs("my_list"), BEFORE, gs("World"), gs("There")).get();
+     * assert length > 0L;
+     * }
+ */ + CompletableFuture linsert( + GlideString key, InsertPosition position, GlideString pivot, GlideString element); + + /** + * Pops an element from the head of the first list that is non-empty, with the given keys + * being checked in the order that they are given.
* Blocks the connection when there are no elements to pop from any of the given lists. * - * @see redis.io for details. - * @apiNote BLPOP is a client blocking command, see Blocking - * Commands for more details and best practices. + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BLPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for details. * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BLPOP operation to - * complete. A value of 0 will block indefinitely. - * @return An array containing the key from which the element was popped - * and the value of the popped element, formatted as [key, value]. - * If no element could be popped and the timeout expired, returns
null. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key from which the element + * was popped and the value of the popped element, formatted as + * [key, value]. If no element could be popped and the timeout expired, returns + * null. * @example *
{@code
      * String[] response = client.blpop(["list1", "list2"], 0.5).get();
@@ -301,20 +772,56 @@ CompletableFuture linsert(
     CompletableFuture blpop(String[] keys, double timeout);
 
     /**
-     * Pops an element from the tail of the first list that is non-empty, with the given keys being
-     * checked in the order that they are given.
+ * Pops an element from the head of the first list that is non-empty, with the given keys + * being checked in the order that they are given.
* Blocks the connection when there are no elements to pop from any of the given lists. * - * @see redis.io for details. - * @apiNote BRPOP is a client blocking command, see Blocking - * Commands for more details and best practices. + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BLPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for details. * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BRPOP operation to - * complete. A value of 0 will block indefinitely. - * @return An array containing the key from which the element was popped - * and the value of the popped element, formatted as [key, value]. - * If no element could be popped and the timeout expired, returns null. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key from which the element + * was popped and the value of the popped element, formatted as + * [key, value]. If no element could be popped and the timeout expired, returns + * null. + * @example + *
{@code
+     * GlideString[] response = client.blpop([gs("list1"), gs("list2")], 0.5).get();
+     * assert response[0].equals(gs("list1"));
+     * assert response[1].equals(gs("element"));
+     * }
+ */ + CompletableFuture blpop(GlideString[] keys, double timeout); + + /** + * Pops an element from the tail of the first list that is non-empty, with the given keys + * being checked in the order that they are given.
+ * Blocks the connection when there are no elements to pop from any of the given lists. + * + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BRPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for details. + * @param keys The keys of the lists to pop from. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key from which the element + * was popped and the value of the popped element, formatted as + * [key, value]. If no element could be popped and the timeout expired, returns + * null. * @example *
{@code
      * String[] response = client.brpop(["list1", "list2"], 0.5).get();
@@ -325,10 +832,41 @@ CompletableFuture linsert(
     CompletableFuture brpop(String[] keys, double timeout);
 
     /**
-     * Inserts specified values at the tail of the list, only if key already
-     * exists and holds a list.
+     * Pops an element from the tail of the first list that is non-empty, with the given keys
+     *  being checked in the order that they are given.
+ * Blocks the connection when there are no elements to pop from any of the given lists. + * + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BRPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
* - * @see redis.io for details. + * @see valkey.io for details. + * @param keys The keys of the lists to pop from. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key from which the element + * was popped and the value of the popped element, formatted as + * [key, value]. If no element could be popped and the timeout expired, returns + * null. + * @example + *
{@code
+     * GlideString[] response = client.brpop([gs("list1"), gs("list2")], 0.5).get();
+     * assert response[0].equals(gs("list1"));
+     * assert response[1].equals(gs("element"));
+     * }
+ */ + CompletableFuture brpop(GlideString[] keys, double timeout); + + /** + * Inserts all the specified values at the tail of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @see valkey.io for details. * @param key The key of the list. * @param elements The elements to insert at the tail of the list stored at key. * @return The length of the list after the push operation. @@ -341,10 +879,28 @@ CompletableFuture linsert( CompletableFuture rpushx(String key, String[] elements); /** - * Inserts specified values at the head of the list, only if key already - * exists and holds a list. + * Inserts all the specified values at the tail of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long listLength = client.rpushx(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert listLength >= 2L;
+     * }
+ */ + CompletableFuture rpushx(GlideString key, GlideString[] elements); + + /** + * Inserts all the specified values at the head of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the list. * @param elements The elements to insert at the head of the list stored at key. * @return The length of the list after the push operation. @@ -355,4 +911,430 @@ CompletableFuture linsert( * }
*/ CompletableFuture lpushx(String key, String[] elements); + + /** + * Inserts all the specified values at the head of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long listLength = client.lpushx(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert listLength >= 2L;
+     * }
+ */ + CompletableFuture lpushx(GlideString key, GlideString[] elements); + + /** + * Pops one or more elements from the first non-empty list from the provided keys + * . + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param count The maximum number of popped elements. + * @return A Map of key name mapped array of popped elements. + * @example + *
{@code
+     * client.lpush("testKey", new String[] {"one", "two", "three"}).get();
+     * Map result = client.lmpop(new String[] {"testKey"}, PopDirection.LEFT, 1L).get();
+     * String[] resultValue = result.get("testKey");
+     * assertArrayEquals(new String[] {"three"}, resultValue);
+     * }
+ */ + CompletableFuture> lmpop( + String[] keys, ListDirection direction, long count); + + /** + * Pops one or more elements from the first non-empty list from the provided keys + * . + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param count The maximum number of popped elements. + * @return A Map of key name mapped array of popped elements. + * @example + *
{@code
+     * client.lpush(gs("testKey"), new GlideString[] {gs("one"), gs("two"), gs("three")}).get();
+     * Map result = client.lmpop(new GlideString[] {gs("testKey")}, PopDirection.LEFT, 1L).get();
+     * GlideString[] resultValue = result.get(gs("testKey"));
+     * assertArrayEquals(new GlideString[] {gs("three")}, resultValue);
+     * }
+ */ + CompletableFuture> lmpop( + GlideString[] keys, ListDirection direction, long count); + + /** + * Pops one element from the first non-empty list from the provided keys. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @return A Map of key name mapped array of the popped element. + * @example + *
{@code
+     * client.lpush("testKey", new String[] {"one", "two", "three"}).get();
+     * Map result = client.lmpop(new String[] {"testKey"}, PopDirection.LEFT).get();
+     * String[] resultValue = result.get("testKey");
+     * assertArrayEquals(new String[] {"three"}, resultValue);
+     * }
+ */ + CompletableFuture> lmpop(String[] keys, ListDirection direction); + + /** + * Pops one element from the first non-empty list from the provided keys. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @return A Map of key name mapped array of the popped element. + * @example + *
{@code
+     * client.lpush(gs("testKey"), new GlideString[] {gs("one"), gs("two"), gs("three")}).get();
+     * Map result = client.lmpop(new GlideString[] {gs("testKey")}, PopDirection.LEFT).get();
+     * GlideString[] resultValue = result.get(gs("testKey"));
+     * assertArrayEquals(new GlideString[] {gs("three")}, resultValue);
+     * }
+ */ + CompletableFuture> lmpop( + GlideString[] keys, ListDirection direction); + + /** + * Blocks the connection until it pops one or more elements from the first non-empty list from the + * provided keys BLMPOP is the blocking variant of {@link + * #lmpop(String[], ListDirection, long)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BLMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param count The maximum number of popped elements. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A Map of key name mapped array of popped elements.
+ * If no member could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * client.lpush("testKey", new String[] {"one", "two", "three"}).get();
+     * Map result = client.blmpop(new String[] {"testKey"}, PopDirection.LEFT, 1L, 0.1).get();
+     * String[] resultValue = result.get("testKey");
+     * assertArrayEquals(new String[] {"three"}, resultValue);
+     * }
+ */ + CompletableFuture> blmpop( + String[] keys, ListDirection direction, long count, double timeout); + + /** + * Blocks the connection until it pops one or more elements from the first non-empty list from the + * provided keys BLMPOP is the blocking variant of {@link + * #lmpop(String[], ListDirection, long)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BLMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param count The maximum number of popped elements. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A Map of key name mapped array of popped elements.
+ * If no member could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * client.lpush(gs("testKey"), new GlideString[] {gs("one"), gs("two"), gs("three")}).get();
+     * Map result = client.blmpop(new GlideString[] {gs("testKey")}, PopDirection.LEFT, 1L, 0.1).get();
+     * GlideString[] resultValue = result.get(gs("testKey"));
+     * assertArrayEquals(new GlideString[] {gs("three")}, resultValue);
+     * }
+ */ + CompletableFuture> blmpop( + GlideString[] keys, ListDirection direction, long count, double timeout); + + /** + * Blocks the connection until it pops one element from the first non-empty list from the provided + * keys BLMPOP is the blocking variant of {@link #lmpop(String[], + * ListDirection)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BLMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A Map of key name mapped array of the popped element.
+ * If no member could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * client.lpush("testKey", new String[] {"one", "two", "three"}).get();
+     * Map result = client.blmpop(new String[] {"testKey"}, PopDirection.LEFT, 0.1).get();
+     * String[] resultValue = result.get("testKey");
+     * assertArrayEquals(new String[] {"three"}, resultValue);
+     * }
+ */ + CompletableFuture> blmpop( + String[] keys, ListDirection direction, double timeout); + + /** + * Blocks the connection until it pops one element from the first non-empty list from the provided + * keys BLMPOP is the blocking variant of {@link #lmpop(String[], + * ListDirection)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BLMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A Map of key name mapped array of the popped element.
+ * If no member could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * client.lpush(gs("testKey"), new GlideString[] {gs("one"), gs("two"), gs("three")}).get();
+     * Map result = client.blmpop(new GlideString[] {gs("testKey")}, PopDirection.LEFT, 0.1).get();
+     * GlideString[] resultValue = result.get(gs("testKey"));
+     * assertArrayEquals(new GlideString[] {gs("three")}, resultValue);
+     * }
+ */ + CompletableFuture> blmpop( + GlideString[] keys, ListDirection direction, double timeout); + + /** + * Sets the list element at index to element.
+ * The index is zero-based, so 0 means the first element, 1 the second + * element and so on. Negative indices can be used to designate elements starting at the tail of + * the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param index The index of the element in the list to be set. + * @return OK. + * @example + *
{@code
+     * String response = client.lset("testKey", 1, "two").get();
+     * assertEquals(response, "OK");
+     * }
+ */ + CompletableFuture lset(String key, long index, String element); + + /** + * Sets the list element at index to element.
+ * The index is zero-based, so 0 means the first element, 1 the second + * element and so on. Negative indices can be used to designate elements starting at the tail of + * the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * @see valkey.io for details. + * @param key The key of the list. + * @param index The index of the element in the list to be set. + * @return OK. + * @example + *
{@code
+     * String response = client.lset(gs("testKey"), 1, gs("two")).get();
+     * assertEquals(response, "OK");
+     * }
+ */ + CompletableFuture lset(GlideString key, long index, GlideString element); + + /** + * Atomically pops and removes the left/right-most element to the list stored at source + * depending on wherefrom, and pushes the element at the first/last element + * of the list stored at destination depending on wherefrom. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param wherefrom The {@link ListDirection} the element should be removed from. + * @param whereto The {@link ListDirection} the element should be added to. + * @return The popped element or null if source does not exist. + * @example + *
{@code
+     * client.lpush("testKey1", new String[] {"two", "one"}).get();
+     * client.lpush("testKey2", new String[] {"four", "three"}).get();
+     * var result = client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT).get();
+     * assertEquals(result, "one");
+     * String[] upratedArray1 = client.lrange("testKey1", 0, -1).get();
+     * String[] upratedArray2 = client.lrange("testKey2", 0, -1).get();
+     * assertArrayEquals(new String[] {"two"}, updatedArray1);
+     * assertArrayEquals(new String[] {"one", "three", "four"}, updatedArray2);
+     * }
+ */ + CompletableFuture lmove( + String source, String destination, ListDirection wherefrom, ListDirection whereto); + + /** + * Atomically pops and removes the left/right-most element to the list stored at source + * depending on wherefrom, and pushes the element at the first/last element + * of the list stored at destination depending on wherefrom. + * + * @since Valkey 6.2.0 and above. + * @apiNote When in cluster mode, source and destination must map to the + * same hash slot. + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param wherefrom The {@link ListDirection} the element should be removed from. + * @param whereto The {@link ListDirection} the element should be added to. + * @return The popped element or null if source does not exist. + * @example + *
{@code
+     * client.lpush(gs("testKey1"), new GlideString[] {gs("two"), gs("one")}).get();
+     * client.lpush(gs("testKey2"), new GlideString[] {gs("four"), gs("three")}).get();
+     * var result = client.lmove(gs("testKey1"), gs("testKey2"), ListDirection.LEFT, ListDirection.LEFT).get();
+     * assertEquals(result, gs("one"));
+     * GlideString[] upratedArray1 = client.lrange(gs("testKey1"), 0, -1).get();
+     * GlideString[] upratedArray2 = client.lrange(gs("testKey2"), 0, -1).get();
+     * assertArrayEquals(new GlideString[] {gs("two")}, updatedArray1);
+     * assertArrayEquals(new GlideString[] {gs("one"), gs("three"), gs("four)"}, updatedArray2);
+     * }
+ */ + CompletableFuture lmove( + GlideString source, GlideString destination, ListDirection wherefrom, ListDirection whereto); + + /** + * Blocks the connection until it pops atomically and removes the left/right-most element to the + * list stored at source depending on wherefrom, and pushes the element + * at the first/last element of the list stored at destination depending on + * wherefrom.
+ * BLMove is the blocking variant of {@link #lmove(String, String, ListDirection, + * ListDirection)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote + *
    + *
  1. When in cluster mode, all source and destination must map + * to the same hash slot. + *
  2. BLMove is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param wherefrom The {@link ListDirection} the element should be removed from. + * @param whereto The {@link ListDirection} the element should be added to. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return The popped element or null if source does not exist or if the + * operation timed-out. + * @example + *
{@code
+     * client.lpush("testKey1", new String[] {"two", "one"}).get();
+     * client.lpush("testKey2", new String[] {"four", "three"}).get();
+     * var result = client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1).get();
+     * assertEquals(result, "one");
+     * String[] upratedArray1 = client.lrange("testKey1", 0, -1).get();
+     * String[] upratedArray2 = client.lrange("testKey2", 0, -1).get();
+     * assertArrayEquals(new String[] {"two"}, updatedArray1);
+     * assertArrayEquals(new String[] {"one", "three", "four"}, updatedArray2);
+     * }
+ */ + CompletableFuture blmove( + String source, + String destination, + ListDirection wherefrom, + ListDirection whereto, + double timeout); + + /** + * Blocks the connection until it pops atomically and removes the left/right-most element to the + * list stored at source depending on wherefrom, and pushes the element + * at the first/last element of the list stored at destination depending on + * wherefrom.
+ * BLMove is the blocking variant of {@link #lmove(String, String, ListDirection, + * ListDirection)}. + * + * @since Valkey 6.2.0 and above. + * @apiNote + *
    + *
  1. When in cluster mode, all source and destination must map + * to the same hash slot. + *
  2. BLMove is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param wherefrom The {@link ListDirection} the element should be removed from. + * @param whereto The {@link ListDirection} the element should be added to. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return The popped element or null if source does not exist or if the + * operation timed-out. + * @example + *
{@code
+     * client.lpush(gs("testKey1"), new GlideString[] {gs("two"), gs("one")}).get();
+     * client.lpush(gs("testKey2"), new GlideString[] {gs("four"), gs("three")}).get();
+     * var result = client.blmove(gs("testKey1"), gs("testKey2"), ListDirection.LEFT, ListDirection.LEFT, 0.1).get();
+     * assertEquals(result, gs("one"));
+     * GlideString[] upratedArray1 = client.lrange(gs("testKey1"), 0, -1).get();
+     * GlideString[] upratedArray2 = client.lrange(gs("testKey2"), 0, -1).get();
+     * assertArrayEquals(new GlideString[] {gs("two")}, updatedArray1);
+     * assertArrayEquals(new GlideString[] {gs("one"), gs("three"), gs("four")}, updatedArray2);
+     * }
+ */ + CompletableFuture blmove( + GlideString source, + GlideString destination, + ListDirection wherefrom, + ListDirection whereto, + double timeout); } diff --git a/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java b/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java new file mode 100644 index 0000000000..6906d98e06 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/PubSubBaseCommands.java @@ -0,0 +1,43 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.GlideString; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands for the "Pub/Sub" group for standalone and cluster clients. + * + * @see Pub/Sub Commands + */ +public interface PubSubBaseCommands { + + /** + * Publishes message on pubsub channel. + * + * @see valkey.io for details. + * @param message The message to publish. + * @param channel The channel to publish the message on. + * @return OK. + * @example + *
{@code
+     * String response = client.publish("The cat said 'meow'!", "announcements").get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture publish(String message, String channel); + + /** + * Publishes message on pubsub channel. + * + * @see valkey.io for details. + * @param message The message to publish. + * @param channel The channel to publish the message on. + * @return OK. + * @example + *
{@code
+     * String response = client.publish(gs("The cat said 'meow'!"), gs("announcements")).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture publish(GlideString message, GlideString channel); +} diff --git a/java/client/src/main/java/glide/api/commands/PubSubClusterCommands.java b/java/client/src/main/java/glide/api/commands/PubSubClusterCommands.java new file mode 100644 index 0000000000..a904f179e0 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/PubSubClusterCommands.java @@ -0,0 +1,49 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.GlideString; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands for the "Pub/Sub" group for a cluster client. + * + * @see Pub/Sub Commands + */ +public interface PubSubClusterCommands { + + /** + * Publishes message on pubsub channel. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param message The message to publish. + * @param channel The channel to publish the message on. + * @param sharded Indicates that this should be run in sharded mode. Setting sharded + * to true is only applicable with Valkey 7.0+. + * @return OK. + * @example + *
{@code
+     * String response = client.publish("The cat said 'meow'!", "announcements", true).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture publish(String message, String channel, boolean sharded); + + /** + * Publishes message on pubsub channel. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param message The message to publish. + * @param channel The channel to publish the message on. + * @param sharded Indicates that this should be run in sharded mode. Setting sharded + * to true is only applicable with Valkey 7.0+. + * @return OK. + * @example + *
{@code
+     * String response = client.publish(gs("The cat said 'meow'!"), gs("announcements"), true).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture publish(GlideString message, GlideString channel, boolean sharded); +} diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java new file mode 100644 index 0000000000..fbc370a276 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java @@ -0,0 +1,131 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.GlideString; +import glide.api.models.configuration.ReadFrom; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands and transactions for the "Scripting and Function" group for standalone and + * cluster clients. + * + * @see Scripting and Function Commands + */ +public interface ScriptingAndFunctionsBaseCommands { + + /** + * Invokes a previously loaded function.
+ * This command is routed to primary nodes only.
+ * To route to a replica please refer to {@link #fcallReadOnly}. + * + * @apiNote When in cluster mode + *
    + *
  • all keys must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random primary node. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param keys An array of keys accessed by the function. To ensure the correct + * execution of functions, both in standalone and clustered deployments, all names of keys + * that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"};
+     * Object response = client.fcall("Deep_Thought", new String[0], args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(String function, String[] keys, String[] arguments); + + /** + * Invokes a previously loaded function.
+ * This command is routed to primary nodes only.
+ * To route to a replica please refer to {@link #fcallReadOnly}. + * + * @apiNote When in cluster mode + *
    + *
  • all keys must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random primary node. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param keys An array of keys accessed by the function. To ensure the correct + * execution of functions, both in standalone and clustered deployments, all names of keys + * that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcall(gs("Deep_Thought"), new GlideString[0], args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall( + GlideString function, GlideString[] keys, GlideString[] arguments); + + /** + * Invokes a previously loaded read-only function.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @apiNote When in cluster mode + *
    + *
  • all keys must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random node. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param keys An array of keys accessed by the function. To ensure the correct + * execution of functions, both in standalone and clustered deployments, all names of keys + * that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"};
+     * Object response = client.fcallReadOnly("Deep_Thought", new String[0], args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(String function, String[] keys, String[] arguments); + + /** + * Invokes a previously loaded read-only function.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @apiNote When in cluster mode + *
    + *
  • all keys must map to the same hash slot. + *
  • if no keys are given, command will be routed to a random node. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param keys An array of keys accessed by the function. To ensure the correct + * execution of functions, both in standalone and clustered deployments, all names of keys + * that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcallReadOnly(gs("Deep_Thought"), new GlideString[0], args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly( + GlideString function, GlideString[] keys, GlideString[] arguments); +} diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java new file mode 100644 index 0000000000..885fc16b81 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -0,0 +1,1032 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.ClusterValue; +import glide.api.models.GlideString; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.configuration.ReadFrom; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands and transactions for the "Scripting and Function" group for a cluster client. + * + * @see Scripting and Function Commands + */ +public interface ScriptingAndFunctionsClusterCommands { + + /** + * Loads a library to Valkey.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @return The library name that was loaded. + * @example + *
{@code
+     * String code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)";
+     * String response = client.functionLoad(code, true).get();
+     * assert response.equals("mylib");
+     * }
+ */ + CompletableFuture functionLoad(String libraryCode, boolean replace); + + /** + * Loads a library to Valkey.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @return The library name that was loaded. + * @example + *
{@code
+     * GlideString code = gs("#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)");
+     * GlideString response = client.functionLoad(code, true).get();
+     * assert response.equals(gs("mylib"));
+     * }
+ */ + CompletableFuture functionLoad(GlideString libraryCode, boolean replace); + + /** + * Loads a library to Valkey. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The library name that was loaded. + * @example + *
{@code
+     * String code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)";
+     * Route route = new SlotKeyRoute("key", PRIMARY);
+     * String response = client.functionLoad(code, true, route).get();
+     * assert response.equals("mylib");
+     * }
+ */ + CompletableFuture functionLoad(String libraryCode, boolean replace, Route route); + + /** + * Loads a library to Valkey. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The library name that was loaded. + * @example + *
{@code
+     * GlideString code = gs("#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)");
+     * Route route = new SlotKeyRoute("key", PRIMARY);
+     * GlideString response = client.functionLoad(code, true, route).get();
+     * assert response.equals(gs("mylib"));
+     * }
+ */ + CompletableFuture functionLoad( + GlideString libraryCode, boolean replace, Route route); + + /** + * Returns information about the functions and libraries.
+ * The command will be routed to a random node. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(boolean withCode); + + /** + * Returns information about the functions and libraries.
+ * The command will be routed to a random node. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get(gs("library_name")), libraryInfo.get(gs("engine")));
+     *     Map[] functions = (Map[]) libraryInfo.get(gs("functions"));
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get(gs("flags"));
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get(gs("name")), gs(String.join(", ", flags)), function.get(gs("description")));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get(gs("library_code")));
+     * }
+     * }
+ */ + CompletableFuture[]> functionListBinary(boolean withCode); + + /** + * Returns information about the functions and libraries.
+ * The command will be routed to a random node. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList("myLib?_backup", true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(String libNamePattern, boolean withCode); + + /** + * Returns information about the functions and libraries.
+ * The command will be routed to a random node. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(gs("myLib?_backup"), true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get(gs("library_name")), libraryInfo.get(gs("engine")));
+     *     Map[] functions = (Map[]) libraryInfo.get(gs("functions"));
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get(gs("flags"));
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get(gs("name")), gs(String.join(", ", flags)), function.get(gs("description")));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get(gs("library_code")));
+     * }
+     * }
+ */ + CompletableFuture[]> functionListBinary( + GlideString libNamePattern, boolean withCode); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * ClusterValue[]> response = client.functionList(true, ALL_NODES).get();
+     * for (String node : response.getMultiValue().keySet()) {
+     *   for (Map libraryInfo : response.getMultiValue().get(node)) {
+     *     System.out.printf("Node '%s' has library '%s' which runs on %s engine%n",
+     *         node, libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture[]>> functionList( + boolean withCode, Route route); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * ClusterValue[]> response = client.functionList(true, ALL_NODES).get();
+     * for (String node : response.getMultiValue().keySet()) {
+     *   for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get(gs("library_name")), libraryInfo.get(gs("engine")));
+     *     Map[] functions = (Map[]) libraryInfo.get(gs("functions"));
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get(gs("flags"));
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get(gs("name")), gs(String.join(", ", flags)), function.get(gs("description")));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get(gs("library_code")));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture[]>> functionListBinary( + boolean withCode, Route route); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * ClusterValue[]> response = client.functionList("myLib?_backup", true, ALL_NODES).get();
+     * for (String node : response.getMultiValue().keySet()) {
+     *   for (Map libraryInfo : response.getMultiValue().get(node)) {
+     *     System.out.printf("Node '%s' has library '%s' which runs on %s engine%n",
+     *         node, libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String.join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture[]>> functionList( + String libNamePattern, boolean withCode, Route route); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * ClusterValue[]> response = client.functionList(gs("myLib?_backup"), true, ALL_NODES).get();
+     * for (String node : response.getMultiValue().keySet()) {
+     *   for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get(gs("library_name")), libraryInfo.get(gs("engine")));
+     *     Map[] functions = (Map[]) libraryInfo.get(gs("functions"));
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get(gs("flags"));
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get(gs("name")), gs(String.join(", ", flags)), function.get(gs("description")));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get(gs("library_code")));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture[]>> functionListBinary( + GlideString libNamePattern, boolean withCode, Route route); + + /** + * Deletes all function libraries.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(); + + /** + * Deletes all function libraries.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(SYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(FlushMode mode); + + /** + * Deletes all function libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(Route route); + + /** + * Deletes all function libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(SYNC, RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(FlushMode mode, Route route); + + /** + * Deletes a library and all its functions.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libName The library name to delete. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete("myLib").get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(String libName); + + /** + * Deletes a library and all its functions.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libName The library name to delete. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete(gs("myLib")).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(GlideString libName); + + /** + * Deletes a library and all its functions. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libName The library name to delete. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete("myLib", RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(String libName, Route route); + + /** + * Deletes a library and all its functions. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libName The library name to delete. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete(gs("myLib"), RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(GlideString libName, Route route); + + /** + * Returns the serialized payload of all loaded libraries.
+ * The command will be routed to a random node. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump().get();
+     * // data can be used to restore loaded functions on any Valkey instance
+     * }
+ */ + CompletableFuture functionDump(); + + /** + * Returns the serialized payload of all loaded libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump(RANDOM).get().getSingleValue();
+     * // data can be used to restore loaded functions on any Valkey instance
+     * }
+ */ + CompletableFuture> functionDump(Route route); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, FunctionRestorePolicy policy); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump(Route)}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, ALL_PRIMARIES).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, Route route); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump(Route)}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH, ALL_PRIMARIES).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore( + byte[] payload, FunctionRestorePolicy policy, Route route); + + /** + * Invokes a previously loaded function.
+ * The command will be routed to a primary random node.
+ * To route to a replica please refer to {@link #fcallReadOnly(String)}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcall("Deep_Thought").get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(String function); + + /** + * Invokes a previously loaded function.
+ * The command will be routed to a primary random node.
+ * To route to a replica please refer to {@link #fcallReadOnly(String)}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcall(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(GlideString function); + + /** + * Invokes a previously loaded function. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * ClusterValue response = client.fcall("Deep_Thought", ALL_NODES).get();
+     * for (Object nodeResponse : response.getMultiValue().values()) {
+     *   assert nodeResponse == 42L;
+     * }
+     * }
+     */
+    CompletableFuture> fcall(String function, Route route);
+
+    /**
+     * Invokes a previously loaded function.
+     *
+     * @since Valkey 7.0 and above.
+     * @see valkey.io for details.
+     * @param function The function name.
+     * @param route Specifies the routing configuration for the command. The client will route the
+     *     command to the nodes defined by route.
+     * @return The invoked function's return value wrapped by a {@link ClusterValue}.
+     * @example
+     *     
{@code
+     * ClusterValue response = client.fcall(gs("Deep_Thought"), ALL_NODES).get();
+     * for (Object nodeResponse : response.getMultiValue().values()) {
+     *   assert nodeResponse == 42L;
+     * }
+     * }
+     */
+    CompletableFuture> fcall(GlideString function, Route route);
+
+    /**
+     * Invokes a previously loaded function.
+ * The command will be routed to a random primary node.
+ * To route to a replica please refer to {@link #fcallReadOnly(String, String[])}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything" };
+     * Object response = client.fcall("Deep_Thought", args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(String function, String[] arguments); + + /** + * Invokes a previously loaded function.
+ * The command will be routed to a random primary node.
+ * To route to a replica please refer to {@link #fcallReadOnly(String, String[])}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcall(gs("Deep_Thought"), args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(GlideString function, GlideString[] arguments); + + /** + * Invokes a previously loaded function. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything" };
+     * ClusterValue response = client.fcall("Deep_Thought", args, RANDOM).get();
+     * assert response.getSingleValue() == 42L;
+     * }
+     */
+    CompletableFuture> fcall(String function, String[] arguments, Route route);
+
+    /**
+     * Invokes a previously loaded function.
+     *
+     * @since Valkey 7.0 and above.
+     * @see valkey.io for details.
+     * @param function The function name.
+     * @param arguments An array of function arguments. arguments
+     *      should not represent names of keys.
+     * @param route Specifies the routing configuration for the command. The client will route the
+     *     command to the nodes defined by route.
+     * @return The invoked function's return value wrapped by a {@link ClusterValue}.
+     * @example
+     *     
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * ClusterValue response = client.fcall(gs("Deep_Thought"), args, RANDOM).get();
+     * assert response.getSingleValue() == 42L;
+     * }
+     */
+    CompletableFuture> fcall(
+            GlideString function, GlideString[] arguments, Route route);
+
+    /**
+     * Invokes a previously loaded read-only function.
+ * The command is routed to a random node depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcallReadOnly("Deep_Thought").get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(String function); + + /** + * Invokes a previously loaded read-only function.
+ * The command is routed to a random node depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcallReadOnly(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(GlideString function); + + /** + * Invokes a previously loaded read-only function. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * ClusterValue response = client.fcallReadOnly("Deep_Thought", ALL_NODES).get();
+     * for (Object nodeResponse : response.getMultiValue().values()) {
+     *   assert nodeResponse == 42L;
+     * }
+     * }
+     */
+    CompletableFuture> fcallReadOnly(String function, Route route);
+
+    /**
+     * Invokes a previously loaded read-only function.
+     *
+     * @since Valkey 7.0 and above.
+     * @see valkey.io for details.
+     * @param function The function name.
+     * @param route Specifies the routing configuration for the command. The client will route the
+     *     command to the nodes defined by route.
+     * @return The invoked function's return value wrapped by a {@link ClusterValue}.
+     * @example
+     *     
{@code
+     * ClusterValue response = client.fcallReadOnly(gs("Deep_Thought"), ALL_NODES).get();
+     * for (Object nodeResponse : response.getMultiValue().values()) {
+     *   assert nodeResponse == 42L;
+     * }
+     * }
+     */
+    CompletableFuture> fcallReadOnly(GlideString function, Route route);
+
+    /**
+     * Invokes a previously loaded function.
+ * The command is routed to a random node depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything" };
+     * Object response = client.fcallReadOnly("Deep_Thought", args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(String function, String[] arguments); + + /** + * Invokes a previously loaded function.
+ * The command is routed to a random node depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return The invoked function's return value. + * @example + *
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * Object response = client.fcallReadOnly(gs("Deep_Thought"), args).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(GlideString function, GlideString[] arguments); + + /** + * Invokes a previously loaded read-only function. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The invoked function's return value wrapped by a {@link ClusterValue}. + * @example + *
{@code
+     * String[] args = new String[] { "Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything" };
+     * ClusterValue response = client.fcallReadOnly("Deep_Thought", args, RANDOM).get();
+     * assert response.getSingleValue() == 42L;
+     * }
+     */
+    CompletableFuture> fcallReadOnly(
+            String function, String[] arguments, Route route);
+
+    /**
+     * Invokes a previously loaded read-only function.
+     *
+     * @since Valkey 7.0 and above.
+     * @see valkey.io for details.
+     * @param function The function name.
+     * @param arguments An array of function arguments. arguments
+     *      should not represent names of keys.
+     * @param route Specifies the routing configuration for the command. The client will route the
+     *     command to the nodes defined by route.
+     * @return The invoked function's return value wrapped by a {@link ClusterValue}.
+     * @example
+     *     
{@code
+     * GlideString[] args = new GlideString[] { gs("Answer"), gs("to"), gs("the"), gs("Ultimate"), gs("Question"), gs("of"), gs("Life,"), gs("the"), gs("Universe,"), gs("and"), gs("Everything")};
+     * ClusterValue response = client.fcallReadOnly(gs("Deep_Thought"), args, RANDOM).get();
+     * assert response.getSingleValue() == 42L;
+     * }
+     */
+    CompletableFuture> fcallReadOnly(
+            GlideString function, GlideString[] arguments, Route route);
+
+    /**
+     * Kills a function that is currently executing.
+ * FUNCTION KILL terminates read-only functions only.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return OK if function is terminated. Otherwise, throws an error. + * @example + *
{@code
+     * String response = client.functionKill().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionKill(); + + /** + * Kills a function that is currently executing.
+ * FUNCTION KILL terminates read-only functions only. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK if function is terminated. Otherwise, throws an error. + * @example + *
{@code
+     * String response = client.functionKill(RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionKill(Route route); + + /** + * Returns information about the function that's currently running and information about the + * available execution engines.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return A Map with two keys: + *
    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ * See example for more details. + * @example + *
{@code
+     * Map>> response = client.functionStats().get().getMultiValue();
+     * for (String node : response.keySet()) {
+     *   Map runningScriptInfo = response.get(node).get("running_script");
+     *   if (runningScriptInfo != null) {
+     *     String[] commandLine = (String[]) runningScriptInfo.get("command");
+     *     System.out.printf("Node '%s' is currently running function '%s' with command line '%s', which has been running for %d ms%n",
+     *         node, runningScriptInfo.get("name"), String.join(" ", commandLine), (long) runningScriptInfo.get("duration_ms"));
+     *   }
+     *   Map enginesInfo = response.get(node).get("engines");
+     *   for (String engineName : enginesInfo.keySet()) {
+     *     Map engine = (Map) enginesInfo.get(engineName);
+     *     System.out.printf("Node '%s' supports engine '%s', which has %d libraries and %d functions in total%n",
+     *         node, engineName, engine.get("libraries_count"), engine.get("functions_count"));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture>>> functionStats(); + + /** + * Returns information about the function that's currently running and information about the + * available execution engines.
+ * The command will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return A Map with two keys: + *
    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ * See example for more details. + * @example + *
{@code
+     * Map>> response = client.functionStatsBinary().get().getMultiValue();
+     * for (GlideString node : response.keySet()) {
+     *   Map runningScriptInfo = response.get(node).get(gs("running_script"));
+     *   if (runningScriptInfo != null) {
+     *     GlideString[] commandLine = (GlideString[]) runningScriptInfo.get(gs("command"));
+     *     System.out.printf("Node '%s' is currently running function '%s' with command line '%s', which has been running for %d ms%n",
+     *         node, runningScriptInfo.get(gs("name")), String.join(" ", Arrays.toString(commandLine)), (long) runningScriptInfo.get(gs("duration_ms")));
+     *   }
+     *   Map enginesInfo = response.get(node).get(gs("engines"));
+     *   for (String engineName : enginesInfo.keySet()) {
+     *     Map engine = (Map) enginesInfo.get(engineName);
+     *     System.out.printf("Node '%s' supports engine '%s', which has %d libraries and %d functions in total%n",
+     *         node, engineName, engine.get(gs("libraries_count")), engine.get(gs("functions_count")));
+     *   }
+     * }
+     * }
+ */ + CompletableFuture>>> functionStatsBinary(); + + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A Map with two keys: + *
    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ * See example for more details. + * @example + *
{@code
+     * Map> response = client.functionStats(RANDOM).get().getSingleValue();
+     * Map runningScriptInfo = response.get("running_script");
+     * if (runningScriptInfo != null) {
+     *   String[] commandLine = (String[]) runningScriptInfo.get("command");
+     *   System.out.printf("Node is currently running function '%s' with command line '%s', which has been running for %d ms%n",
+     *       runningScriptInfo.get("name"), String.join(" ", commandLine), (long)runningScriptInfo.get("duration_ms"));
+     * }
+     * Map enginesInfo = response.get("engines");
+     * for (String engineName : enginesInfo.keySet()) {
+     *   Map engine = (Map) enginesInfo.get(engineName);
+     *   System.out.printf("Node supports engine '%s', which has %d libraries and %d functions in total%n",
+     *       engineName, engine.get("libraries_count"), engine.get("functions_count"));
+     * }
+     * }
+ */ + CompletableFuture>>> functionStats(Route route); + + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A Map with two keys: + *
    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ * See example for more details. + * @example + *
{@code
+     * Map> response = client.functionStats(RANDOM).get().getSingleValue();
+     * Map runningScriptInfo = response.get(gs("running_script"));
+     * if (runningScriptInfo != null) {
+     *   GlideString[] commandLine = (GlideString[]) runningScriptInfo.get(gs("command"));
+     *   System.out.printf("Node is currently running function '%s' with command line '%s', which has been running for %d ms%n",
+     *       runningScriptInfo.get(gs("name")), String.join(" ", Arrays.toString(commandLine)), (long)runningScriptInfo.get(gs("duration_ms")));
+     * }
+     * Map enginesInfo = response.get(gs("engines"));
+     * for (GlideString engineName : enginesInfo.keySet()) {
+     *   Map engine = (Map) enginesInfo.get(engineName);
+     *   System.out.printf("Node supports engine '%s', which has %d libraries and %d functions in total%n",
+     *       engineName, engine.get(gs("libraries_count")), engine.get(gs("functions_count")));
+     * }
+     * }
+ */ + CompletableFuture>>> functionStatsBinary( + Route route); +} diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java new file mode 100644 index 0000000000..f6ca4890bb --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -0,0 +1,405 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.GlideString; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.configuration.ReadFrom; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands and transactions for the "Scripting and Function" group for a standalone + * client. + * + * @see Scripting and Function Commands + */ +public interface ScriptingAndFunctionsCommands { + + /** + * Loads a library to Valkey. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @return The library name that was loaded. + * @example + *
{@code
+     * String code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)";
+     * String response = client.functionLoad(code, true).get();
+     * assert response.equals("mylib");
+     * }
+ */ + CompletableFuture functionLoad(String libraryCode, boolean replace); + + /** + * Loads a library to Valkey. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @return The library name that was loaded. + * @example + *
{@code
+     * GlideString code = gs("#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)");
+     * GlideString response = client.functionLoad(code, true).get();
+     * assert response.equals(gs("mylib"));
+     * }
+ */ + CompletableFuture functionLoad(GlideString libraryCode, boolean replace); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String. join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(boolean withCode); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about all libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList(true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String. join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionListBinary(boolean withCode); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList("myLib?_backup", true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String. join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionList(String libNamePattern, boolean withCode); + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Info about queried libraries and their functions. + * @example + *
{@code
+     * Map[] response = client.functionList("myLib?_backup", true).get();
+     * for (Map libraryInfo : response) {
+     *     System.out.printf("Server has library '%s' which runs on %s engine%n",
+     *         libraryInfo.get("library_name"), libraryInfo.get("engine"));
+     *     Map[] functions = (Map[]) libraryInfo.get("functions");
+     *     for (Map function : functions) {
+     *         Set flags = (Set) function.get("flags");
+     *         System.out.printf("Library has function '%s' with flags '%s' described as %s%n",
+     *             function.get("name"), String. join(", ", flags), function.get("description"));
+     *     }
+     *     System.out.printf("Library code:%n%s%n", libraryInfo.get("library_code"));
+     * }
+     * }
+ */ + CompletableFuture[]> functionListBinary( + GlideString libNamePattern, boolean withCode); + + /** + * Deletes all function libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(); + + /** + * Deletes all function libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(SYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(FlushMode mode); + + /** + * Deletes a library and all its functions. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libName The library name to delete. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete("myLib").get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(String libName); + + /** + * Deletes a library and all its functions. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param libName The library name to delete. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete(gs("myLib")).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(GlideString libName); + + /** + * Returns the serialized payload of all loaded libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump().get();
+     * // now data could be saved to restore loaded functions on any Valkey instance
+     * }
+ */ + CompletableFuture functionDump(); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, FunctionRestorePolicy policy); + + /** + * Invokes a previously loaded function.
+ * This command is routed to primary nodes only.
+ * To route to a replica please refer to {@link #fcallReadOnly}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcall("Deep_Thought").get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(String function); + + /** + * Invokes a previously loaded function.
+ * This command is routed to primary nodes only.
+ * To route to a replica please refer to {@link #fcallReadOnly}. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcall(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcall(GlideString function); + + /** + * Invokes a previously loaded read-only function.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcallReadOnly("Deep_Thought").get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(String function); + + /** + * Invokes a previously loaded read-only function.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param function The function name. + * @return The invoked function's return value. + * @example + *
{@code
+     * Object response = client.fcallReadOnly(gs("Deep_Thought")).get();
+     * assert response == 42L;
+     * }
+ */ + CompletableFuture fcallReadOnly(GlideString function); + + /** + * Kills a function that is currently executing.
+ * FUNCTION KILL terminates read-only functions only. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return OK if function is terminated. Otherwise, throws an error. + * @example + *
{@code
+     * String response = client.functionKill().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionKill(); + + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return A Map with two keys: + *
    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ * See example for more details. + * @example + *
{@code
+     * Map> response = client.functionStats().get();
+     * Map runningScriptInfo = response.get("running_script");
+     * if (runningScriptInfo != null) {
+     *   String[] commandLine = (String[]) runningScriptInfo.get("command");
+     *   System.out.printf("Server is currently running function '%s' with command line '%s', which has been running for %d ms%n",
+     *       runningScriptInfo.get("name"), String.join(" ", commandLine), (long)runningScriptInfo.get("duration_ms"));
+     * }
+     * Map enginesInfo = response.get("engines");
+     * for (String engineName : enginesInfo.keySet()) {
+     *   Map engine = (Map) enginesInfo.get(engineName);
+     *   System.out.printf("Server supports engine '%s', which has %d libraries and %d functions in total%n",
+     *       engineName, engine.get("libraries_count"), engine.get("functions_count"));
+     * }
+     * }
+ */ + CompletableFuture>> functionStats(); + + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return A Map with two keys: + *
    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ * See example for more details. + * @example + *
{@code
+     * Map> response = client.functionStats().get();
+     * Map runningScriptInfo = response.get(gs("running_script"));
+     * if (runningScriptInfo != null) {
+     *   GlideString[] commandLine = (GlideString[]) runningScriptInfo.get(gs("command"));
+     *   System.out.printf("Server is currently running function '%s' with command line '%s', which has been running for %d ms%n",
+     *       runningScriptInfo.get(gs("name")), String.join(" ", Arrays.toString(commandLine)), (long)runningScriptInfo.get(gs("duration_ms")));
+     * }
+     * Map enginesInfo = response.get(gs("engines"));
+     * for (GlideString engineName : enginesInfo.keySet()) {
+     *   Map engine = (Map) enginesInfo.get(gs(engineName));
+     *   System.out.printf("Server supports engine '%s', which has %d libraries and %d functions in total%n",
+     *       engineName, engine.get(gs("libraries_count")), engine.get(gs("functions_count")));
+     * }
+     * }
+ */ + CompletableFuture>> functionStatsBinary(); +} diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java index db05056651..2d78572173 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java @@ -1,7 +1,8 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; import glide.api.models.ClusterValue; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -12,17 +13,17 @@ * Supports commands and transactions for the "Server Management Commands" group for a cluster * client. * - * @see Server Management Commands + * @see Server Management Commands */ public interface ServerManagementClusterCommands { /** - * Gets information and statistics about the Redis server using the {@link Section#DEFAULT} - * option. The command will be routed to all primary nodes. + * Gets information and statistics about the server using the {@link Section#DEFAULT} option. The + * command will be routed to all primary nodes. * - * @see redis.io for details. - * @return Response from Redis cluster with a Map{@literal } with - * each address as the key and its corresponding value is the information for the node. + * @see valkey.io for details. + * @return A Map{@literal } with each address as the key and its + * corresponding value is the information for the node. * @example *
{@code
      * ClusterValue payload = clusterClient.info().get();
@@ -35,14 +36,14 @@ public interface ServerManagementClusterCommands {
     CompletableFuture> info();
 
     /**
-     * Gets information and statistics about the Redis server. If no argument is provided, so the
-     * {@link Section#DEFAULT} option is assumed.
+     * Gets information and statistics about the server. If no argument is provided, so the {@link
+     * Section#DEFAULT} option is assumed.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param route Specifies the routing configuration for the command. The client will route the
      *     command to the nodes defined by route.
-     * @return Response from Redis cluster with a String with the requested Sections.
-     *     When specifying a route other than a single node, it returns a 
+     * @return A String containing the information for the default sections. When
+     *     specifying a route other than a single node, it returns a 
      *     Map{@literal } with each address as the key and its corresponding
      *     value is the information for the node.
      * @example
@@ -57,16 +58,15 @@ public interface ServerManagementClusterCommands {
     CompletableFuture> info(Route route);
 
     /**
-     * Gets information and statistics about the Redis server. The command will be routed to all
-     * primary nodes.
+     * Gets information and statistics about the server. The command will be routed to all primary
+     * nodes.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param options A list of {@link InfoOptions.Section} values specifying which sections of
      *     information to retrieve. When no parameter is provided, the {@link
      *     InfoOptions.Section#DEFAULT} option is assumed.
-     * @return Response from Redis cluster with a Map{@literal } with
-     *     each address as the key and its corresponding value is the information of the sections
-     *     requested for the node.
+     * @return A Map{@literal } with each address as the key and its
+     *     corresponding value is the information of the sections requested for the node.
      * @example
      *     
{@code
      * ClusterValue payload = clusterClient.info(InfoOptions.builder().section(STATS).build()).get();
@@ -79,15 +79,15 @@ public interface ServerManagementClusterCommands {
     CompletableFuture> info(InfoOptions options);
 
     /**
-     * Gets information and statistics about the Redis server.
+     * Gets information and statistics about the server.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param options A list of {@link InfoOptions.Section} values specifying which sections of
      *     information to retrieve. When no parameter is provided, the {@link
      *     InfoOptions.Section#DEFAULT} option is assumed.
      * @param route Specifies the routing configuration for the command. The client will route the
      *     command to the nodes defined by route.
-     * @return Response from Redis cluster with a String with the requested sections.
+     * @return A String with the containing the information for the sections requested.
      *     When specifying a route other than a single node, it returns a 
      *     Map{@literal } with each address as the key and its corresponding
      *     value is the information of the sections requested for the node.
@@ -104,7 +104,7 @@ public interface ServerManagementClusterCommands {
      * Rewrites the configuration file with the current configuration.
* The command will be routed automatically to all nodes. * - * @see redis.io for details. + * @see valkey.io for details. * @return OK when the configuration was rewritten properly, otherwise an error is * thrown. * @example @@ -118,7 +118,7 @@ public interface ServerManagementClusterCommands { /** * Rewrites the configuration file with the current configuration. * - * @see redis.io for details. + * @see valkey.io for details. * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. * @return OK when the configuration was rewritten properly, otherwise an error is @@ -133,12 +133,12 @@ public interface ServerManagementClusterCommands { CompletableFuture configRewrite(Route route); /** - * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands.
+ * Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands.
* The command will be routed automatically to all nodes. * - * @see redis.io for details. + * @see valkey.io for details. * @return OK to confirm that the statistics were successfully reset. * @example *
{@code
@@ -149,11 +149,11 @@ public interface ServerManagementClusterCommands {
     CompletableFuture configResetStat();
 
     /**
-     * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands.
+     * Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param route Specifies the routing configuration for the command. The client will route the
      *     command to the nodes defined by route.
      * @return OK to confirm that the statistics were successfully reset.
@@ -167,10 +167,10 @@ public interface ServerManagementClusterCommands {
     CompletableFuture configResetStat(Route route);
 
     /**
-     * Reads the configuration parameters of a running Redis server.
+ * Get the values of configuration parameters.
* The command will be sent to a random node. * - * @see redis.io for details. + * @see valkey.io for details. * @param parameters An array of configuration parameter names to retrieve values * for. * @return A map of values corresponding to the configuration parameters. @@ -184,9 +184,9 @@ public interface ServerManagementClusterCommands { CompletableFuture> configGet(String[] parameters); /** - * Reads the configuration parameters of a running Redis server. + * Get the values of configuration parameters. * - * @see redis.io for details. + * @see valkey.io for details. * @param parameters An array of configuration parameter names to retrieve values * for. * @param route Specifies the routing configuration for the command. The client will route the @@ -210,7 +210,7 @@ public interface ServerManagementClusterCommands { * Sets configuration parameters to the specified values.
* The command will be sent to all nodes. * - * @see redis.io for details. + * @see valkey.io for details. * @param parameters A map consisting of configuration parameters and their * respective values to set. * @return OK if all configurations have been successfully set. Otherwise, raises an @@ -226,7 +226,7 @@ public interface ServerManagementClusterCommands { /** * Sets configuration parameters to the specified values. * - * @see redis.io for details. + * @see valkey.io for details. * @param parameters A map consisting of configuration parameters and their * respective values to set. * @param route Specifies the routing configuration for the command. The client will route the @@ -245,7 +245,7 @@ public interface ServerManagementClusterCommands { * Returns the server time.
* The command will be routed to a random node. * - * @see redis.io for details. + * @see valkey.io for details. * @return The current server time as a String array with two elements: A * UNIX TIME and the amount of microseconds already elapsed in the current second. The * returned array is in a [UNIX TIME, Microseconds already elapsed] format. @@ -260,7 +260,7 @@ public interface ServerManagementClusterCommands { /** * Returns the server time. * - * @see redis.io for details. + * @see valkey.io for details. * @param route Specifies the routing configuration for the command. The client will route the * command to the nodes defined by route. * @return The current server time as a String array with two elements: A @@ -288,7 +288,7 @@ public interface ServerManagementClusterCommands { * was made since then.
* The command will be routed to a random node. * - * @see redis.io for details. + * @see valkey.io for details. * @return UNIX TIME of the last DB save executed with success. * @example *
{@code
@@ -302,7 +302,7 @@ public interface ServerManagementClusterCommands {
      * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save
      * was made since then.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param route Specifies the routing configuration for the command. The client will route the
      *     command to the nodes defined by route.
      * @return UNIX TIME of the last DB save executed with success.
@@ -315,4 +315,343 @@ public interface ServerManagementClusterCommands {
      * }
*/ CompletableFuture> lastsave(Route route); + + /** + * Deletes all the keys of all the existing databases. This command never fails.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.flushall().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushall(); + + /** + * Deletes all the keys of all the existing databases. This command never fails.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.flushall(ASYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushall(FlushMode mode); + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * Route route = new SlotKeyRoute("key", PRIMARY);
+     * String response = client.flushall(route).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushall(Route route); + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * Route route = new SlotKeyRoute("key", PRIMARY);
+     * String response = client.flushall(SYNC, route).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushall(FlushMode mode, Route route); + + /** + * Deletes all the keys of the currently selected database. This command never fails.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.flushdb().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushdb(); + + /** + * Deletes all the keys of the currently selected database. This command never fails.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.flushdb(ASYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushdb(FlushMode mode); + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * Route route = new SlotKeyRoute("key", PRIMARY);
+     * String response = client.flushdb(route).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushdb(Route route); + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * Route route = new SlotKeyRoute("key", PRIMARY);
+     * String response = client.flushdb(SYNC, route).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushdb(FlushMode mode, Route route); + + /** + * Displays a piece of generative computer art and the Valkey version.
+ * The command will be routed to a random node. + * + * @see valkey.io for details. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut().get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(); + + /** + * Displays a piece of generative computer art and the Valkey version.
+ * The command will be routed to a random node. + * + * @see valkey.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Valkey version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Valkey version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int[] parameters); + + /** + * Displays a piece of generative computer art and the Valkey version.
+ * The command will be routed to a random node. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(6).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int version); + + /** + * Displays a piece of generative computer art and the Valkey version.
+ * The command will be routed to a random node. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(6, new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * data = client.lolwut(5, new int[] { 30, 5, 5 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     *
+     * }
+ */ + CompletableFuture lolwut(int version, int[] parameters); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * ClusterValue response = client.lolwut(ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(Route route); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @see valkey.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Valkey version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Valkey version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(new int[] { 40, 20 }, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(int[] parameters, Route route); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * ClusterValue response = client.lolwut(6, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(int version, Route route); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(6, new int[] { 40, 20 }, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * data = client.lolwut(5, new int[] { 30, 5, 5 }, ALL_NODES).get();
+     * for (String data : response.getMultiValue().values()) {
+     *     System.out.println(data);
+     *     assert data.contains("Redis ver. 7.2.3");
+     * }
+     * }
+ */ + CompletableFuture> lolwut(int version, int[] parameters, Route route); + + /** + * Returns the number of keys in the database.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @return The total number of keys across the primary nodes. + * @example + *
{@code
+     * Long numKeys = client.dbsize().get();
+     * System.out.printf("Number of keys across the primary nodes: %d%n", numKeys);
+     * }
+ */ + CompletableFuture dbsize(); + + /** + * Returns the number of keys in the database. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return The number of keys in the database.
+ * If the query is routed to multiple nodes, returns the sum of the number of keys across all + * routed nodes. + * @example + *
{@code
+     * Route route = new ByAddressRoute("localhost", 8000);
+     * Long numKeys = client.dbsize(route).get();
+     * System.out.printf("Number of keys for node at port 8000: %d%n", numKeys);
+     * }
+ */ + CompletableFuture dbsize(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java index 30dd8f15a2..68b4ac42a0 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.InfoOptions.Section; import java.util.Map; @@ -9,17 +10,18 @@ /** * Supports commands and transactions for the "Server Management" group for a standalone client. * - * @see Server Management Commands + * @see Server Management Commands */ public interface ServerManagementCommands { + /** A keyword for {@link #lolwut(int)} and {@link #lolwut(int, int[])}. */ + String VERSION_VALKEY_API = "VERSION"; + /** - * Gets information and statistics about the Redis server using the {@link Section#DEFAULT} - * option. + * Gets information and statistics about the server using the {@link Section#DEFAULT} option. * - * @see redis.io for details. - * @return Response from Redis containing a String with the information for the - * default sections. + * @see valkey.io for details. + * @return A String with the information for the default sections. * @example *
{@code
      * String response = client.info().get();
@@ -29,13 +31,12 @@ public interface ServerManagementCommands {
     CompletableFuture info();
 
     /**
-     * Get information and statistics about the Redis server.
+     * Get information and statistics about the server.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param options A list of {@link Section} values specifying which sections of information to
      *     retrieve. When no parameter is provided, the {@link Section#DEFAULT} option is assumed.
-     * @return Response from Redis containing a String with the information for the
-     *     sections requested.
+     * @return A String containing the information for the sections requested.
      * @example
      *     
{@code
      * String response = regularClient.info(InfoOptions.builder().section(STATS).build()).get();
@@ -45,9 +46,9 @@ public interface ServerManagementCommands {
     CompletableFuture info(InfoOptions options);
 
     /**
-     * Changes the currently selected Redis database.
+     * Changes the currently selected database.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param index The index of the database to select.
      * @return A simple OK response.
      * @example
@@ -61,7 +62,7 @@ public interface ServerManagementCommands {
     /**
      * Rewrites the configuration file with the current configuration.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @return OK when the configuration was rewritten properly, otherwise an error is
      *     thrown.
      * @example
@@ -73,11 +74,11 @@ public interface ServerManagementCommands {
     CompletableFuture configRewrite();
 
     /**
-     * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands.
+     * Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @return OK to confirm that the statistics were successfully reset.
      * @example
      *     
{@code
@@ -88,9 +89,9 @@ public interface ServerManagementCommands {
     CompletableFuture configResetStat();
 
     /**
-     * Reads the configuration parameters of a running Redis server.
+     * Get the values of configuration parameters.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param parameters An array of configuration parameter names to retrieve values
      *     for.
      * @return A map of values corresponding to the configuration parameters.
@@ -106,7 +107,7 @@ public interface ServerManagementCommands {
     /**
      * Sets configuration parameters to the specified values.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @param parameters A map consisting of configuration parameters and their
      *     respective values to set.
      * @return OK if all configurations have been successfully set. Otherwise, raises an
@@ -122,7 +123,7 @@ public interface ServerManagementCommands {
     /**
      * Returns the server time.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @return The current server time as a String array with two elements: A 
      *     UNIX TIME and the amount of microseconds already elapsed in the current second. The
      *     returned array is in a [UNIX TIME, Microseconds already elapsed] format.
@@ -138,7 +139,7 @@ public interface ServerManagementCommands {
      * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save
      * was made since then.
      *
-     * @see redis.io for details.
+     * @see valkey.io for details.
      * @return UNIX TIME of the last DB save executed with success.
      * @example
      *     
{@code
@@ -147,4 +148,151 @@ public interface ServerManagementCommands {
      * }
*/ CompletableFuture lastsave(); + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.flushall().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushall(); + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.flushall(ASYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushall(FlushMode mode); + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.flushdb().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushdb(); + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.flushdb(ASYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture flushdb(FlushMode mode); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @see valkey.io for details. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut().get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @see valkey.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On Valkey version 5, those are length of the line, number of squares per + * row, and number of squares per column. + *
  • On Valkey version 6, those are number of columns and number of lines. + *
  • On other versions parameters are ignored. + *
+ * + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int[] parameters); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(6).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int version); + + /** + * Displays a piece of generative computer art and the Valkey version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @return A piece of generative computer art along with the current Valkey version. + * @example + *
{@code
+     * String data = client.lolwut(6, new int[] { 40, 20 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * data = client.lolwut(5, new int[] { 30, 5, 5 }).get();
+     * System.out.println(data);
+     * assert data.contains("Redis ver. 7.2.3");
+     * }
+ */ + CompletableFuture lolwut(int version, int[] parameters); + + /** + * Returns the number of keys in the currently selected database. + * + * @see valkey.io for details. + * @return The number of keys in the currently selected database. + * @example + *
{@code
+     * Long numKeys = client.dbsize().get();
+     * System.out.printf("Number of keys in the current database: %d%n", numKeys);
+     * }
+ */ + CompletableFuture dbsize(); } diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index a143d2e82f..33385aa898 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -1,6 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.SScanOptionsBinary; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -8,14 +11,17 @@ * Supports commands and transactions for the "Set Commands" group for standalone and cluster * clients. * - * @see Set Commands + * @see Set Commands */ public interface SetBaseCommands { + /** Valkey API keyword used to limit calculation of intersection of sorted sets. */ + String SET_LIMIT_VALKEY_API = "LIMIT"; + /** * Adds specified members to the set stored at key. Specified members that are * already a member of this set are ignored. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key where members will be added to its set. * @param members A list of members to add to the set stored at key. * @return The number of members that were added to the set, excluding members already present. @@ -29,11 +35,29 @@ public interface SetBaseCommands { */ CompletableFuture sadd(String key, String[] members); + /** + * Adds specified members to the set stored at key. Specified members that are + * already a member of this set are ignored. + * + * @see valkey.io for details. + * @param key The key where members will be added to its set. + * @param members A list of members to add to the set stored at key. + * @return The number of members that were added to the set, excluding members already present. + * @remarks If key does not exist, a new set is created before adding members + * . + * @example + *
{@code
+     * Long result = client.sadd(gs("my_set"), new GlideString[]{gs("member1"), gs("member2")}).get();
+     * assert result == 2L;
+     * }
+ */ + CompletableFuture sadd(GlideString key, GlideString[] members); + /** * Removes specified members from the set stored at key. Specified members that are * not a member of this set are ignored. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key from which members will be removed. * @param members A list of members to remove from the set stored at key. * @return The number of members that were removed from the set, excluding non-existing members. @@ -47,10 +71,28 @@ public interface SetBaseCommands { */ CompletableFuture srem(String key, String[] members); + /** + * Removes specified members from the set stored at key. Specified members that are + * not a member of this set are ignored. + * + * @see valkey.io for details. + * @param key The key from which members will be removed. + * @param members A list of members to remove from the set stored at key. + * @return The number of members that were removed from the set, excluding non-existing members. + * @remarks If key does not exist, it is treated as an empty set and this command + * returns 0. + * @example + *
{@code
+     * Long result = client.srem(gs("my_set"), new GlideString[]{gs("member1"), gs("member2")}).get();
+     * assert result == 2L;
+     * }
+ */ + CompletableFuture srem(GlideString key, GlideString[] members); + /** * Retrieves all the members of the set value stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key from which to retrieve the set members. * @return A Set of all members of the set. * @remarks If key does not exist an empty set will be returned. @@ -62,10 +104,25 @@ public interface SetBaseCommands { */ CompletableFuture> smembers(String key); + /** + * Retrieves all the members of the set value stored at key. + * + * @see valkey.io for details. + * @param key The key from which to retrieve the set members. + * @return A Set of all members of the set. + * @remarks If key does not exist an empty set will be returned. + * @example + *
{@code
+     * Set result = client.smembers(gs("my_set")).get();
+     * assert result.equals(Set.of(gs("member1"), gs("member2"), gs("member3")));
+     * }
+ */ + CompletableFuture> smembers(GlideString key); + /** * Retrieves the set cardinality (number of elements) of the set stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key from which to retrieve the number of set members. * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. * @example @@ -76,10 +133,24 @@ public interface SetBaseCommands { */ CompletableFuture scard(String key); + /** + * Retrieves the set cardinality (number of elements) of the set stored at key. + * + * @see valkey.io for details. + * @param key The key from which to retrieve the number of set members. + * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. + * @example + *
{@code
+     * Long result = client.scard("my_set").get();
+     * assert result == 3L;
+     * }
+ */ + CompletableFuture scard(GlideString key); + /** * Checks whether each member is contained in the members of the set stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the set to check. * @param members A list of members to check for existence in the set. * @return An array of Boolean values, each indicating if the respective @@ -92,14 +163,30 @@ public interface SetBaseCommands { */ CompletableFuture smismember(String key, String[] members); + /** + * Checks whether each member is contained in the members of the set stored at key. + * + * @see valkey.io for details. + * @param key The key of the set to check. + * @param members A list of members to check for existence in the set. + * @return An array of Boolean values, each indicating if the respective + * member exists in the set. + * @example + *
{@code
+     * Boolean[] areMembers = client.smismembmer(gs("my_set"), new GlideString[] { gs("a"), gs("b"), gs("c") }).get();
+     * assert areMembers[0] && areMembers[1] && !areMembers[2]; // Only first two elements are present in "my_set"
+     * }
+ */ + CompletableFuture smismember(GlideString key, GlideString[] members); + /** * Moves member from the set at source to the set at destination * , removing it from the source set. Creates a new destination set if needed. The * operation is atomic. * - * @apiNote When in cluster mode, source and destination must map to the - * same hash slot. - * @see redis.io for details. + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @see valkey.io for details. * @param source The key of the set to remove the element from. * @param destination The key of the set to add the element to. * @param member The set element to move. @@ -113,10 +200,31 @@ public interface SetBaseCommands { */ CompletableFuture smove(String source, String destination, String member); + /** + * Moves member from the set at source to the set at destination + * , removing it from the source set. Creates a new destination set if needed. The + * operation is atomic. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @see valkey.io for details. + * @param source The key of the set to remove the element from. + * @param destination The key of the set to add the element to. + * @param member The set element to move. + * @return true on success, or false if the source set does + * not exist or the element is not a member of the source set. + * @example + *
{@code
+     * Boolean moved = client.smove(gs("set1"), gs("set2"), gs("element")).get();
+     * assert moved;
+     * }
+ */ + CompletableFuture smove(GlideString source, GlideString destination, GlideString member); + /** * Returns if member is a member of the set stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the set. * @param member The member to check for existence in the set. * @return true if the member exists in the set, false otherwise. If @@ -133,13 +241,65 @@ public interface SetBaseCommands { */ CompletableFuture sismember(String key, String member); + /** + * Returns if member is a member of the set stored at key. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param member The member to check for existence in the set. + * @return true if the member exists in the set, false otherwise. If + * key doesn't exist, it is treated as an empty set and the command + * returns false. + * @example + *
{@code
+     * Boolean payload1 = client.sismember(gs("mySet"), gs("member1")).get();
+     * assert payload1; // Indicates that "member1" exists in the set "mySet".
+     *
+     * Boolean payload2 = client.sismember(gs("mySet"), gs("nonExistingMember")).get();
+     * assert !payload2; // Indicates that "nonExistingMember" does not exist in the set "mySet".
+     * }
+ */ + CompletableFuture sismember(GlideString key, GlideString member); + + /** + * Computes the difference between the first set and all the successive sets in keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets to diff. + * @return A Set of elements representing the difference between the sets.
+ * If the a key does not exist, it is treated as an empty set. + * @example + *
{@code
+     * Set values = client.sdiff(new String[] {"set1", "set2"}).get();
+     * assert values.contains("element"); // Indicates that "element" is present in "set1", but missing in "set2"
+     * }
+ */ + CompletableFuture> sdiff(String[] keys); + + /** + * Computes the difference between the first set and all the successive sets in keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets to diff. + * @return A Set of elements representing the difference between the sets.
+ * If the a key does not exist, it is treated as an empty set. + * @example + *
{@code
+     * Set values = client.sdiff(new GlideString[] {gs("set1"), gs("set2")}).get();
+     * assert values.contains(gs("element")); // Indicates that "element" is present in "set1", but missing in "set2"
+     * }
+ */ + CompletableFuture> sdiff(GlideString[] keys); + /** * Stores the difference between the first set and all the successive sets in keys * into a new set at destination. * * @apiNote When in cluster mode, destination and all keys must map to - * the same hash slot. - * @see redis.io for details. + * the same hash slot. + * @see valkey.io for details. * @param destination The key of the destination set. * @param keys The keys of the sets to diff. * @return The number of elements in the resulting set. @@ -151,12 +311,29 @@ public interface SetBaseCommands { */ CompletableFuture sdiffstore(String destination, String[] keys); + /** + * Stores the difference between the first set and all the successive sets in keys + * into a new set at destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. + * @param destination The key of the destination set. + * @param keys The keys of the sets to diff. + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long length = client.sdiffstore(gs("mySet"), new GlideString[] { gs("set1"), gs("set2") }).get();
+     * assert length == 5L;
+     * }
+ */ + CompletableFuture sdiffstore(GlideString destination, GlideString[] keys); + /** * Gets the intersection of all the given sets. * - * @apiNote When in cluster mode, all keys must map to the same hash slot - * . - * @see redis.io for details. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. * @param keys The keys of the sets. * @return A Set of members which are present in all given sets.
* If one or more sets do not exist, an empty set will be returned. @@ -171,11 +348,124 @@ public interface SetBaseCommands { */ CompletableFuture> sinter(String[] keys); + /** + * Gets the intersection of all the given sets. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return A Set of members which are present in all given sets.
+ * If one or more sets do not exist, an empty set will be returned. + * @example + *
{@code
+     * Set values = client.sinter(new GlideString[] {gs("set1"), gs("set2")}).get();
+     * assert values.contains(gs("element")); // Indicates that these sets have a common element
+     *
+     * Set values = client.sinter(new GlideString[] {gs("set1"), gs("nonExistingSet")}).get();
+     * assert values.size() == 0;
+     * }
+ */ + CompletableFuture> sinter(GlideString[] keys); + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return The cardinality of the intersection result. If one or more sets do not exist, 0 + * is returned. + * @example + *
{@code
+     * Long response = client.sintercard(new String[] {"set1", "set2"}).get();
+     * assertEquals(2L, response);
+     *
+     * Long emptyResponse = client.sintercard(new String[] {"set1", "nonExistingSet"}).get();
+     * assertEquals(emptyResponse, 0L);
+     * }
+ */ + CompletableFuture sintercard(String[] keys); + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return The cardinality of the intersection result. If one or more sets do not exist, 0 + * is returned. + * @example + *
{@code
+     * Long response = client.sintercard(new GlideString[] {gs("set1"), gs("set2")}).get();
+     * assertEquals(2L, response);
+     *
+     * Long emptyResponse = client.sintercard(new GlideString[] {gs("set1"), gs("nonExistingSet")}).get();
+     * assertEquals(emptyResponse, 0L);
+     * }
+ */ + CompletableFuture sintercard(GlideString[] keys); + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @param limit The limit for the intersection cardinality value. + * @return The cardinality of the intersection result. If one or more sets do not exist, 0 + * is returned. If the intersection cardinality reaches limit partway + * through the computation, returns limit as the cardinality. + * @example + *
{@code
+     * Long response = client.sintercard(new String[] {"set1", "set2"}, 3).get();
+     * assertEquals(2L, response);
+     *
+     * Long emptyResponse = client.sintercard(new String[] {"set1", "nonExistingSet"}, 3).get();
+     * assertEquals(emptyResponse, 0L);
+     *
+     * // when intersection cardinality > limit, returns limit as cardinality
+     * Long response2 = client.sintercard(new String[] {"set3", "set4"}, 3).get();
+     * assertEquals(3L, response2);
+     * }
+ */ + CompletableFuture sintercard(String[] keys, long limit); + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @param limit The limit for the intersection cardinality value. + * @return The cardinality of the intersection result. If one or more sets do not exist, 0 + * is returned. If the intersection cardinality reaches limit partway + * through the computation, returns limit as the cardinality. + * @example + *
{@code
+     * Long response = client.sintercard(new GlideString[] {gs("set1"), gs("set2")}, 3).get();
+     * assertEquals(2L, response);
+     *
+     * Long emptyResponse = client.sintercard(new GlideString[] {gs("set1"), gs("nonExistingSet")}, 3).get();
+     * assertEquals(emptyResponse, 0L);
+     *
+     * // when intersection cardinality > limit, returns limit as cardinality
+     * Long response2 = client.sintercard(new GlideString[] {gs("set3"), gs("set4")}, 3).get();
+     * assertEquals(3L, response2);
+     * }
+ */ + CompletableFuture sintercard(GlideString[] keys, long limit); + /** * Stores the members of the intersection of all given sets specified by keys into a * new set at destination. * - * @see redis.io for details. + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. * @param destination The key of the destination set. * @param keys The keys from which to retrieve the set members. * @return The number of elements in the resulting set. @@ -187,13 +477,31 @@ public interface SetBaseCommands { */ CompletableFuture sinterstore(String destination, String[] keys); + /** + * Stores the members of the intersection of all given sets specified by keys into a + * new set at destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. + * @param destination The key of the destination set. + * @param keys The keys from which to retrieve the set members. + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long length = client.sinterstore(gs("mySet"), new GlideString[] { gs("set1"), gs("set2") }).get();
+     * assert length == 5L;
+     * }
+ */ + CompletableFuture sinterstore(GlideString destination, GlideString[] keys); + /** * Stores the members of the union of all given sets specified by keys into a new set * at destination. * * @apiNote When in cluster mode, destination and all keys must map to - * the same hash slot. - * @see redis.io for details. + * the same hash slot. + * @see valkey.io for details. * @param destination The key of the destination set. * @param keys The keys from which to retrieve the set members. * @return The number of elements in the resulting set. @@ -204,4 +512,323 @@ public interface SetBaseCommands { * }
*/ CompletableFuture sunionstore(String destination, String[] keys); + + /** + * Stores the members of the union of all given sets specified by keys into a new set + * at destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see valkey.io for details. + * @param destination The key of the destination set. + * @param keys The keys from which to retrieve the set members. + * @return The number of elements in the resulting set. + * @example + *
{@code
+     * Long length = client.sunionstore(gs("mySet"), new GlideString[] { gs("set1"), gs("set2") }).get();
+     * assert length == 5L;
+     * }
+ */ + CompletableFuture sunionstore(GlideString destination, GlideString[] keys); + + /** + * Returns a random element from the set value stored at key. + * + * @see valkey.io for details. + * @param key The key from which to retrieve the set member. + * @return A random element from the set, or null if key does not exist. + * @example + *
{@code
+     * client.sadd("test", new String[] {"one"}).get();
+     * String response = client.srandmember("test").get();
+     * assertEquals("one", response);
+     * }
+ */ + CompletableFuture srandmember(String key); + + /** + * Returns a random element from the set value stored at key. + * + * @see valkey.io for details. + * @param key The key from which to retrieve the set member. + * @return A random element from the set, or null if key does not exist. + * @example + *
{@code
+     * client.sadd(gs("test"), new GlideString[] {gs("one")}).get();
+     * GlideString response = client.srandmember(gs("test")).get();
+     * assertEquals(gs("one"), response);
+     * }
+ */ + CompletableFuture srandmember(GlideString key); + + /** + * Returns one or more random elements from the set value stored at key. + * + * @see valkey.io for details. + * @param key The key from which to retrieve the set members. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return An array of elements from the set, or an empty array if + * key does not exist. + * @example + *
{@code
+     * client.sadd("test", new String[] {"one"}).get();
+     * String[] response = client.srandmember("test", -2).get();
+     * assertArrayEquals(new String[] {"one", "one"}, response);
+     * }
+ */ + CompletableFuture srandmember(String key, long count); + + /** + * Returns one or more random elements from the set value stored at key. + * + * @see valkey.io for details. + * @param key The key from which to retrieve the set members. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return An array of elements from the set, or an empty array if + * key does not exist. + * @example + *
{@code
+     * client.sadd(gs("test"), new GlideString[] {gs("one")}).get();
+     * GlideString[] response = client.srandmember(gs("test"), -2).get();
+     * assertArrayEquals(new GlideString[] {gs("one"), gs("one")}, response);
+     * }
+ */ + CompletableFuture srandmember(GlideString key, long count); + + /** + * Removes and returns one random member from the set stored at key. + * + * @see valkey.io for details. + * @param key The key of the set. + * @return The value of the popped member.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * String value1 = client.spop("mySet").get();
+     * assert value1.equals("value1");
+     *
+     * String value2 = client.spop("nonExistingSet").get();
+     * assert value2.equals(null);
+     * }
+ */ + CompletableFuture spop(String key); + + /** + * Removes and returns one random member from the set stored at key. + * + * @see valkey.io for details. + * @param key The key of the set. + * @return The value of the popped member.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * GlideString value1 = client.spop(gs("mySet")).get();
+     * assert value1.equals(gs("value1"));
+     *
+     * GlideString value2 = client.spop(gs("nonExistingSet")).get();
+     * assert value2.equals(null);
+     * }
+ */ + CompletableFuture spop(GlideString key); + + /** + * Removes and returns up to count random members from the set stored at key + * , depending on the set's length. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param count The count of the elements to pop from the set. + * @return A set of popped elements will be returned depending on the set's length.
+ * If key does not exist, an empty Set will be returned. + * @example + *
{@code
+     * Set values1 = client.spopCount("mySet", 2).get();
+     * assert values1.equals(new String[] {"value1", "value2"});
+     *
+     * Set values2 = client.spopCount("nonExistingSet", 2).get();
+     * assert values2.size() == 0;
+     * }
+ */ + CompletableFuture> spopCount(String key, long count); + + /** + * Removes and returns up to count random members from the set stored at key + * , depending on the set's length. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param count The count of the elements to pop from the set. + * @return A set of popped elements will be returned depending on the set's length.
+ * If key does not exist, an empty Set will be returned. + * @example + *
{@code
+     * Set values1 = client.spopCount(gs("mySet"), 2).get();
+     * assert values1.equals(new GlideString[] {gs("value1"), gs("value2")});
+     *
+     * Set values2 = client.spopCount(gs("nonExistingSet"), 2).get();
+     * assert values2.size() == 0;
+     * }
+ */ + CompletableFuture> spopCount(GlideString key, long count); + + /** + * Gets the union of all the given sets. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return A set of members which are present in at least one of the given sets. If none of the + * sets exist, an empty set will be returned. + * @example + *
{@code
+     * assert client.sadd("my_set1", new String[]{"member1", "member2"}).get() == 2;
+     * assert client.sadd("my_set2", new String[]{"member2", "member3"}).get() == 2;
+     * Set result = client.sunion(new String[] {"my_set1", "my_set2"}).get();
+     * assertEquals(Set.of("member1", "member2", "member3"), result);
+     *
+     * result = client.sunion(new String[] {"my_set1", "non_existent_set"}).get();
+     * assertEquals(Set.of("member1", "member2"), result);
+     * }
+ */ + CompletableFuture> sunion(String[] keys); + + /** + * Gets the union of all the given sets. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return A set of members which are present in at least one of the given sets. If none of the + * sets exist, an empty set will be returned. + * @example + *
{@code
+     * assert client.sadd(gs("my_set1"), new GlideString[]{gs("member1"), gs("member2")}).get() == 2;
+     * assert client.sadd(gs("my_set2"), new GlideString[]{gs("member2"), gs("member3")}).get() == 2;
+     * Set result = client.sunion(new GlideString[] {gs("my_set1"), gs("my_set2")}).get();
+     * assertEquals(Set.of(gs("member1"), gs("member2"), gs("member3")), result);
+     *
+     * result = client.sunion(new GlideString[] {gs("my_set1"), gs("non_existent_set")}).get();
+     * assertEquals(Set.of(gs("member1"), gs("member2")), result);
+     * }
+ */ + CompletableFuture> sunion(GlideString[] keys); + + /** + * Iterates incrementally over a set. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the set. The second element is always an + * Array of the subset of the set held in key. + * @example + *
{@code
+     * // Assume key contains a set with 200 members
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *   result = client.sscan(key1, cursor).get();
+     *   cursor = result[0].toString();
+     *   Object[] stringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nSSCAN iteration:");
+     *   Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture sscan(String key, String cursor); + + /** + * Iterates incrementally over a set. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the set. The second element is always an + * Array of the subset of the set held in key. + * @example + *
{@code
+     * // Assume key contains a set with 200 members
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *   result = client.sscan(key1, cursor).get();
+     *   cursor = gs(result[0].toString());
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nSSCAN iteration:");
+     *   Arrays.asList(glideStringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture sscan(GlideString key, GlideString cursor); + + /** + * Iterates incrementally over a set. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param sScanOptions The {@link SScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the set. The second element is always an + * Array of the subset of the set held in key. + * @example + *
{@code
+     * // Assume key contains a set with 200 members
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *   result = client.sscan(key1, cursor, SScanOptions.builder().matchPattern("*").count(20L).build()).get();
+     *   cursor = result[0].toString();
+     *   Object[] stringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nSSCAN iteration:");
+     *   Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture sscan(String key, String cursor, SScanOptions sScanOptions); + + /** + * Iterates incrementally over a set. + * + * @see valkey.io for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param sScanOptions The {@link SScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the set. The second element is always an + * Array of the subset of the set held in key. + * @example + *
{@code
+     * // Assume key contains a set with 200 members
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *   result = client.sscan(key1, cursor, SScanOptionsBinary.builder().matchPattern(gs("*")).count(20L).build()).get();
+     *   cursor = gs(result[0].toString());
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nSSCAN iteration:");
+     *   Arrays.asList(glideStringResults).stream().forEach(i -> System.out.print(i + ", "));
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture sscan( + GlideString key, GlideString cursor, SScanOptionsBinary sScanOptions); } diff --git a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java index dcd2fce4d5..ce6d4c5973 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; @@ -12,7 +13,16 @@ import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.RangeOptions.ScoreRange; import glide.api.models.commands.RangeOptions.ScoredRangeQuery; -import glide.api.models.commands.ZaddOptions; +import glide.api.models.commands.ScoreFilter; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.WeightAggregateOptions.KeyArrayBinary; +import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; +import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeysBinary; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; +import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.scan.ZScanOptionsBinary; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -20,20 +30,29 @@ * Supports commands and transactions for the "Sorted Set Commands" group for standalone and cluster * clients. * - * @see Sorted Set Commands + * @see Sorted Set Commands */ public interface SortedSetBaseCommands { - public static final String WITH_SCORES_REDIS_API = "WITHSCORES"; - public static final String WITH_SCORE_REDIS_API = "WITHSCORE"; + /** Valkey API keyword used to query sorted set members with their scores. */ + String WITH_SCORES_VALKEY_API = "WITHSCORES"; + + /** Valkey API keyword used to query a sorted set member with its score. */ + String WITH_SCORE_VALKEY_API = "WITHSCORE"; + + /** Valkey API keyword used to extract specific count of members from a sorted set. */ + String COUNT_VALKEY_API = "COUNT"; + + /** Valkey API keyword used to limit calculation of intersection of sorted sets. */ + String LIMIT_VALKEY_API = "LIMIT"; /** * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. - * @param options The Zadd options. + * @param options The ZAdd options. * @param changed Modify the return value from the number of new elements added, to the total * number of elements changed. * @return The number of elements added to the sorted set.
@@ -50,16 +69,45 @@ public interface SortedSetBaseCommands { * }
*/ CompletableFuture zadd( - String key, Map membersScoresMap, ZaddOptions options, boolean changed); + String key, Map membersScoresMap, ZAddOptions options, boolean changed); + + /** + * Adds members with their scores to the sorted set stored at key.
+ * If a member is already a part of the sorted set, its score is updated. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersScoresMap A Map of members to their corresponding scores. + * @param options The ZAdd options. + * @param changed Modify the return value from the number of new elements added, to the total + * number of elements changed. + * @return The number of elements added to the sorted set.
+ * If changed is set, returns the number of elements updated in the sorted set. + * @example + *
{@code
+     * ZaddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
+     * Long num = client.zadd(gs("mySortedSet"), Map.of(gs("member1"), 10.5, gs("member2"), 8.2), options, false).get();
+     * assert num == 2L; // Indicates that two elements have been to the sorted set "mySortedSet".
+     *
+     * options = ZaddOptions.builder().conditionalChange(ONLY_IF_EXISTS).build();
+     * Long num = client.zadd(gs("existingSortedSet"), Map.of(gs("member1"), 15.0, gs("member2"), 5.5), options, false).get();
+     * assert num == 2L; // Updates the scores of two existing members in the sorted set "existingSortedSet".
+     * }
+ */ + CompletableFuture zadd( + GlideString key, + Map membersScoresMap, + ZAddOptions options, + boolean changed); /** * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. - * @param options The Zadd options. + * @param options The ZAdd options. * @return The number of elements added to the sorted set. * @example *
{@code
@@ -73,13 +121,36 @@ CompletableFuture zadd(
      * }
*/ CompletableFuture zadd( - String key, Map membersScoresMap, ZaddOptions options); + String key, Map membersScoresMap, ZAddOptions options); + + /** + * Adds members with their scores to the sorted set stored at key.
+ * If a member is already a part of the sorted set, its score is updated. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersScoresMap A Map of members to their corresponding scores. + * @param options The ZAdd options. + * @return The number of elements added to the sorted set. + * @example + *
{@code
+     * ZaddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
+     * Long num = client.zadd(gs("mySortedSet"), Map.of(gs("member1"), 10.5, gs("member2"), 8.2), options).get();
+     * assert num == 2L; // Indicates that two elements have been added to the sorted set "mySortedSet".
+     *
+     * options = ZaddOptions.builder().conditionalChange(ONLY_IF_EXISTS).build();
+     * Long num = client.zadd(gs("existingSortedSet"), Map.of(gs("member1"), 15.0, gs("member2"), 5.5), options).get();
+     * assert num == 0L; // No new members were added to the sorted set "existingSortedSet".
+     * }
+ */ + CompletableFuture zadd( + GlideString key, Map membersScoresMap, ZAddOptions options); /** * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. * @param changed Modify the return value from the number of new elements added, to the total @@ -98,7 +169,27 @@ CompletableFuture zadd( * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersScoresMap A Map of members to their corresponding scores. + * @param changed Modify the return value from the number of new elements added, to the total + * number of elements changed. + * @return The number of elements added to the sorted set.
+ * If changed is set, returns the number of elements updated in the sorted set. + * @example + *
{@code
+     * Long num = client.zadd(gs("mySortedSet"), Map.of(gs("member1"), 10.5, gs("member2"), 8.2), true).get();
+     * assert num == 2L; // Indicates that two elements have been added or updated in the sorted set "mySortedSet".
+     * }
+ */ + CompletableFuture zadd( + GlideString key, Map membersScoresMap, boolean changed); + + /** + * Adds members with their scores to the sorted set stored at key.
+ * If a member is already a part of the sorted set, its score is updated. + * + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. * @return The number of elements added to the sorted set. @@ -110,35 +201,52 @@ CompletableFuture zadd( */ CompletableFuture zadd(String key, Map membersScoresMap); + /** + * Adds members with their scores to the sorted set stored at key.
+ * If a member is already a part of the sorted set, its score is updated. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersScoresMap A Map of members to their corresponding scores. + * @return The number of elements added to the sorted set. + * @example + *
{@code
+     * Long num = client.zadd(gs("mySortedSet"), Map.of(gs("member1"), 10.5, gs("member2"), 8.2)).get();
+     * assert num == 2L; // Indicates that two elements have been added to the sorted set "mySortedSet".
+     * }
+ */ + CompletableFuture zadd(GlideString key, Map membersScoresMap); + /** * Increments the score of member in the sorted set stored at key by increment * .
* If member does not exist in the sorted set, it is added with * increment as its score (as if its previous score was 0.0).
* If key does not exist, a new sorted set with the specified member as its sole - * member is created. + * member is created.
+ * zaddIncr with empty option acts as {@link #zincrby(String, double, String)}. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member A member in the sorted set to increment. * @param increment The score to increment the member. - * @param options The Zadd options. + * @param options The ZAdd options. * @return The score of the member.
* If there was a conflict with the options, the operation aborts and null is * returned. * @example *
{@code
-     * ZaddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
+     * ZAddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
      * Double num = client.zaddIncr("mySortedSet", member, 5.0, options).get();
      * assert num == 5.0;
      *
-     * options = ZaddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build();
+     * options = ZAddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build();
      * Double num = client.zaddIncr("existingSortedSet", member, 3.0, options).get();
      * assert num == null;
      * }
*/ CompletableFuture zaddIncr( - String key, String member, double increment, ZaddOptions options); + String key, String member, double increment, ZAddOptions options); /** * Increments the score of member in the sorted set stored at key by increment @@ -146,9 +254,43 @@ CompletableFuture zaddIncr( * If member does not exist in the sorted set, it is added with * increment as its score (as if its previous score was 0.0).
* If key does not exist, a new sorted set with the specified member as its sole - * member is created. + * member is created.
+ * zaddIncr with empty option acts as {@link #zincrby(GlideString, double, + * GlideString)}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member A member in the sorted set to increment. + * @param increment The score to increment the member. + * @param options The ZAdd options. + * @return The score of the member.
+ * If there was a conflict with the options, the operation aborts and null is + * returned. + * @example + *
{@code
+     * ZAddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
+     * Double num = client.zaddIncr(gs("mySortedSet"), member, 5.0, options).get();
+     * assert num == 5.0;
+     *
+     * options = ZAddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build();
+     * Double num = client.zaddIncr(gs("existingSortedSet"), member, 3.0, options).get();
+     * assert num == null;
+     * }
+ */ + CompletableFuture zaddIncr( + GlideString key, GlideString member, double increment, ZAddOptions options); + + /** + * Increments the score of member in the sorted set stored at key by increment + * .
+ * If member does not exist in the sorted set, it is added with + * increment as its score (as if its previous score was 0.0).
+ * If key does not exist, a new sorted set with the specified member as its sole + * member is created.
+ * zaddIncr with empty option acts as {@link #zincrby(GlideString, double, + * GlideString)}. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member A member in the sorted set to increment. * @param increment The score to increment the member. @@ -161,11 +303,32 @@ CompletableFuture zaddIncr( */ CompletableFuture zaddIncr(String key, String member, double increment); + /** + * Increments the score of member in the sorted set stored at key by increment + * .
+ * If member does not exist in the sorted set, it is added with + * increment as its score (as if its previous score was 0.0).
+ * If key does not exist, a new sorted set with the specified member as its sole + * member is created. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member A member in the sorted set to increment. + * @param increment The score to increment the member. + * @return The score of the member. + * @example + *
{@code
+     * Double num = client.zaddIncr(gs("mySortedSet"), member, 5.0).get();
+     * assert num == 5.0;
+     * }
+ */ + CompletableFuture zaddIncr(GlideString key, GlideString member, double increment); + /** * Removes the specified members from the sorted set stored at key.
* Specified members that are not a member of this set are ignored. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param members An array of members to remove from the sorted set. * @return The number of members that were removed from the sorted set, not including non-existing @@ -183,10 +346,32 @@ CompletableFuture zaddIncr( */ CompletableFuture zrem(String key, String[] members); + /** + * Removes the specified members from the sorted set stored at key.
+ * Specified members that are not a member of this set are ignored. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members An array of members to remove from the sorted set. + * @return The number of members that were removed from the sorted set, not including non-existing + * members.
+ * If key does not exist, it is treated as an empty sorted set, and this command + * returns 0. + * @example + *
{@code
+     * Long num1 = client.zrem(gs("mySortedSet"), new GlideString[] {gs("member1"), gs("member2")}).get();
+     * assert num1 == 2L; // Indicates that two members have been removed from the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zrem(gs("nonExistingSortedSet"), new GlideString[] {gs("member1"), gs("member2")}).get();
+     * assert num2 == 0L; // Indicates that no members were removed as the sorted set "nonExistingSortedSet" does not exist.
+     * }
+ */ + CompletableFuture zrem(GlideString key, GlideString[] members); + /** * Returns the cardinality (number of elements) of the sorted set stored at key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @return The number of elements in the sorted set.
* If key does not exist, it is treated as an empty sorted set, and this command @@ -202,11 +387,30 @@ CompletableFuture zaddIncr( */ CompletableFuture zcard(String key); + /** + * Returns the cardinality (number of elements) of the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @return The number of elements in the sorted set.
+ * If key does not exist, it is treated as an empty sorted set, and this command + * return 0. + * @example + *
{@code
+     * Long num1 = client.zcard(gs("mySortedSet")).get();
+     * assert num1 == 3L; // Indicates that there are 3 elements in the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zcard((gs("nonExistingSortedSet")).get();
+     * assert num2 == 0L;
+     * }
+ */ + CompletableFuture zcard(GlideString key); + /** * Removes and returns up to count members with the lowest scores from the sorted set * stored at the specified key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param count Specifies the quantity of members to pop.
* If count is higher than the sorted set's cardinality, returns all members and @@ -218,16 +422,37 @@ CompletableFuture zaddIncr( * @example *
{@code
      * Map payload = client.zpopmax("mySortedSet", 2).get();
-     * assert payload.equals(Map.of('member3', 7.5 , 'member2', 8.0)); // Indicates that 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set.
+     * assert payload.equals(Map.of("member3", 7.5 , "member2", 8.0)); // Indicates that "member3" with a score of 7.5 and "member2" with a score of 8.0 have been removed from the sorted set.
      * }
*/ CompletableFuture> zpopmin(String key, long count); + /** + * Removes and returns up to count members with the lowest scores from the sorted set + * stored at the specified key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count Specifies the quantity of members to pop.
+ * If count is higher than the sorted set's cardinality, returns all members and + * their scores, ordered from lowest to highest. + * @return A map of the removed members and their scores, ordered from the one with the lowest + * score to the one with the highest.
+ * If key doesn't exist, it will be treated as an empty sorted set and the + * command returns an empty Map. + * @example + *
{@code
+     * Map payload = client.zpopmax(gs("mySortedSet"), 2).get();
+     * assert payload.equals(Map.of(gs("member3"), 7.5, gs("member2"), 8.0)); // Indicates that gs("member3") with a score of 7.5 and gs("member2") with a score of 8.0 have been removed from the sorted set.
+     * }
+ */ + CompletableFuture> zpopmin(GlideString key, long count); + /** * Removes and returns the member with the lowest score from the sorted set stored at the * specified key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @return A map containing the removed member and its corresponding score.
* If key doesn't exist, it will be treated as an empty sorted set and the @@ -235,16 +460,93 @@ CompletableFuture zaddIncr( * @example *
{@code
      * Map payload = client.zpopmin("mySortedSet").get();
-     * assert payload.equals(Map.of('member1', 5.0)); // Indicates that 'member1' with a score of 5.0 has been removed from the sorted set.
+     * assert payload.equals(Map.of("member1", 5.0)); // Indicates that "member1" with a score of 5.0 has been removed from the sorted set.
      * }
*/ CompletableFuture> zpopmin(String key); + /** + * Removes and returns the member with the lowest score from the sorted set stored at the + * specified key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @return A map containing the removed member and its corresponding score.
+ * If key doesn't exist, it will be treated as an empty sorted set and the + * command returns an empty Map. + * @example + *
{@code
+     * Map payload = client.zpopmin(gs("mySortedSet")).get();
+     * assert payload.equals(Map.of(gs("member1"), 5.0)); // Indicates that gs("member1") with a score of 5.0 has been removed from the sorted set.
+     * }
+ */ + CompletableFuture> zpopmin(GlideString key); + + /** + * Blocks the connection until it removes and returns a member with the lowest score from the + * first non-empty sorted set, with the given keys being checked in the order they + * are provided.
+ * BZPOPMIN is the blocking variant of {@link #zpopmin(String)}.
+ * + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BZPOPMIN is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return An array containing the key where the member was popped out, the member + * itself, and the member score.
+ * If no member could be popped and the timeout expired, returns null + * . + * @example + *
{@code
+     * Object[] data = client.bzpopmin(new String[] {"zset1", "zset2"}, 0.5).get();
+     * System.out.printf("Popped '%s' with score %d from sorted set '%s'%n", data[1], data[2], data[0]);
+     * }
+ */ + CompletableFuture bzpopmin(String[] keys, double timeout); + + /** + * Blocks the connection until it removes and returns a member with the lowest score from the + * first non-empty sorted set, with the given keys being checked in the order they + * are provided.
+ * BZPOPMIN is the blocking variant of {@link #zpopmin(String)}.
+ * + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BZPOPMIN is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return An array containing the key where the member was popped out, the member + * itself, and the member score.
+ * If no member could be popped and the timeout expired, returns null + * . + * @example + *
{@code
+     * Object[] data = client.bzpopmin(new GlideString[] {gs("zset1"), gs("zset2")}, 0.5).get();
+     * System.out.printf("Popped '%s' with score %d from sorted set '%s'%n", data[1], data[2], data[0]);
+     * }
+ */ + CompletableFuture bzpopmin(GlideString[] keys, double timeout); + /** * Removes and returns up to count members with the highest scores from the sorted * set stored at the specified key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param count Specifies the quantity of members to pop.
* If count is higher than the sorted set's cardinality, returns all members and @@ -256,16 +558,37 @@ CompletableFuture zaddIncr( * @example *
{@code
      * Map payload = client.zpopmax("mySortedSet", 2).get();
-     * assert payload.equals(Map.of('member2', 8.0, 'member3', 7.5)); // Indicates that 'member2' with a score of 8.0 and 'member3' with a score of 7.5 have been removed from the sorted set.
+     * assert payload.equals(Map.of("member2", 8.0, "member3", 7.5)); // Indicates that "member2" with a score of 8.0 and "member3" with a score of 7.5 have been removed from the sorted set.
      * }
*/ CompletableFuture> zpopmax(String key, long count); + /** + * Removes and returns up to count members with the highest scores from the sorted + * set stored at the specified key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count Specifies the quantity of members to pop.
+ * If count is higher than the sorted set's cardinality, returns all members and + * their scores, ordered from highest to lowest. + * @return A map of the removed members and their scores, ordered from the one with the highest + * score to the one with the lowest.
+ * If key doesn't exist, it will be treated as an empty sorted set and the + * command returns an empty Map. + * @example + *
{@code
+     * Map payload = client.zpopmax(gs("mySortedSet"), 2).get();
+     * assert payload.equals(Map.of(gs("member2"), 8.0, gs("member3"), 7.5)); // Indicates that gs("member2") with a score of 8.0 and gs("member3") with a score of 7.5 have been removed from the sorted set.
+     * }
+ */ + CompletableFuture> zpopmax(GlideString key, long count); + /** * Removes and returns the member with the highest score from the sorted set stored at the * specified key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @return A map containing the removed member and its corresponding score.
* If key doesn't exist, it will be treated as an empty sorted set and the @@ -273,15 +596,92 @@ CompletableFuture zaddIncr( * @example *
{@code
      * Map payload = client.zpopmax("mySortedSet").get();
-     * assert payload.equals(Map.of('member1', 10.0)); // Indicates that 'member1' with a score of 10.0 has been removed from the sorted set.
+     * assert payload.equals(Map.of("member1", 10.0)); // Indicates that "member1" with a score of 10.0 has been removed from the sorted set.
      * }
*/ CompletableFuture> zpopmax(String key); + /** + * Removes and returns the member with the highest score from the sorted set stored at the + * specified key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @return A map containing the removed member and its corresponding score.
+ * If key doesn't exist, it will be treated as an empty sorted set and the + * command returns an empty Map. + * @example + *
{@code
+     * Map payload = client.zpopmax(gs("mySortedSet")).get();
+     * assert payload.equals(Map.of(gs("member1"), 10.0)); // Indicates that gs("member1") with a score of 10.0 has been removed from the sorted set.
+     * }
+ */ + CompletableFuture> zpopmax(GlideString key); + + /** + * Blocks the connection until it removes and returns a member with the highest score from the + * first non-empty sorted set, with the given keys being checked in the order they + * are provided.
+ * BZPOPMAX is the blocking variant of {@link #zpopmax(String)}.
+ * + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BZPOPMAX is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return An array containing the key where the member was popped out, the member + * itself, and the member score.
+ * If no member could be popped and the timeout expired, returns null + * . + * @example + *
{@code
+     * Object[] data = client.bzpopmax(new String[] {"zset1", "zset2"}, 0.5).get();
+     * System.out.printf("Popped '%s' with score %d from sorted set '%s'%n", data[1], data[2], data[0]);
+     * }
+ */ + CompletableFuture bzpopmax(String[] keys, double timeout); + + /** + * Blocks the connection until it removes and returns a member with the highest score from the + * first non-empty sorted set, with the given keys being checked in the order they + * are provided.
+ * BZPOPMAX is the blocking variant of {@link #zpopmax(String)}.
+ * + * @apiNote + *
    + *
  • When in cluster mode, all keys must map to the same hash slot. + *
  • BZPOPMAX is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return An array containing the key where the member was popped out, the member + * itself, and the member score.
+ * If no member could be popped and the timeout expired, returns null + * . + * @example + *
{@code
+     * Object[] data = client.bzpopmax(new GlideString[] {gs("zset1"), gs("zset2")}, 0.5).get();
+     * System.out.printf("Popped '%s' with score %d from sorted set '%s'%n", data[1], data[2], data[0]);
+     * }
+ */ + CompletableFuture bzpopmax(GlideString[] keys, double timeout); + /** * Returns the score of member in the sorted set stored at key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member The member whose score is to be retrieved. * @return The score of the member.
@@ -299,12 +699,32 @@ CompletableFuture zaddIncr( CompletableFuture zscore(String key, String member); /** - * Returns the specified range of elements in the sorted set stored at key.
- * ZRANGE can perform different types of range queries: by index (rank), by the - * score, or by lexicographical order.
- * To get the elements with their scores, see {@link #zrangeWithScores}. + * Returns the score of member in the sorted set stored at key. * - * @see redis.io for more details. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose score is to be retrieved. + * @return The score of the member.
+ * If member does not exist in the sorted set, null is returned.
+ * If key does not exist, null is returned. + * @example + *
{@code
+     * Double num1 = client.zscore(gs("mySortedSet")), gs("member")).get();
+     * assert num1 == 10.5; // Indicates that the score of "member" in the sorted set "mySortedSet" is 10.5.
+     *
+     * Double num2 = client.zscore(gs("mySortedSet"), gs("nonExistingMember")).get();
+     * assert num2 == null;
+     * }
+ */ + CompletableFuture zscore(GlideString key, GlideString member); + + /** + * Returns the specified range of elements in the sorted set stored at key.
+ * ZRANGE can perform different types of range queries: by index (rank), by the + * score, or by lexicographical order.
+ * To get the elements with their scores, see {@link #zrangeWithScores}. + * + * @see valkey.io for more details. * @param key The key of the sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
*
    @@ -321,11 +741,11 @@ CompletableFuture zaddIncr( *
    {@code
          * RangeByScore query1 = new RangeByScore(new ScoreBoundary(10), new ScoreBoundary(20));
          * String[] payload1 = client.zrange("mySortedSet", query1, true).get(); // Returns members with scores between 10 and 20.
    -     * assert payload1.equals(new String[] {'member3', 'member2', 'member1'}); // Returns all members in descending order.
    +     * assert payload1.equals(new String[] {"member3", "member2", "member1"}); // Returns all members in descending order.
          *
          * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
          * String[] payload2 = client.zrange("mySortedSet", query2, false).get();
    -     * assert payload2.equals(new String[] {'member2', 'member3'}); // Returns members with scores within the range of negative infinity to 3, in ascending order.
    +     * assert payload2.equals(new String[] {"member2", "member3"}); // Returns members with scores within the range of negative infinity to 3, in ascending order.
          * }
    */ CompletableFuture zrange(String key, RangeQuery rangeQuery, boolean reverse); @@ -336,7 +756,39 @@ CompletableFuture zaddIncr( * score, or by lexicographical order.
    * To get the elements with their scores, see {@link #zrangeWithScores}. * - * @see redis.io for more details. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
    + *
      + *
    • For range queries by index (rank), use {@link RangeByIndex}. + *
    • For range queries by lexicographical order, use {@link RangeByLex}. + *
    • For range queries by score, use {@link RangeByScore}. + *
    + * + * @param reverse If true, reverses the sorted set, with index 0 as the element with the highest + * score. + * @return An array of elements within the specified range. If key does not exist, it + * is treated as an empty sorted set, and the command returns an empty array. + * @example + *
    {@code
    +     * RangeByScore query1 = new RangeByScore(new ScoreBoundary(10), new ScoreBoundary(20));
    +     * GlideString[] payload1 = client.zrange(gs("mySortedSet"), query1, true).get(); // Returns members with scores between 10 and 20.
    +     * assert Arrays.equals(payload1, new GlideString[] {gs("member3"), gs("member2"), gs("member1")}); // Returns all members in descending order.
    +     *
    +     * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
    +     * GlideString[] payload2 = client.zrange(gs("mySortedSet"), query2, false).get();
    +     * assert Arrays.equals(payload2, new GlideString[] {gs("member2"), gs("member3")}); // Returns members with scores within the range of negative infinity to 3, in ascending order.
    +     * }
    + */ + CompletableFuture zrange(GlideString key, RangeQuery rangeQuery, boolean reverse); + + /** + * Returns the specified range of elements in the sorted set stored at key.
    + * ZRANGE can perform different types of range queries: by index (rank), by the + * score, or by lexicographical order.
    + * To get the elements with their scores, see {@link #zrangeWithScores}. + * + * @see valkey.io for more details. * @param key The key of the sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
    *
      @@ -351,20 +803,53 @@ CompletableFuture zaddIncr( *
      {@code
            * RangeByIndex query1 = new RangeByIndex(0, -1);
            * String[] payload1 = client.zrange("mySortedSet",query1).get();
      -     * assert payload1.equals(new String[] {'member1', 'member2', 'member3'}); // Returns all members in ascending order.
      +     * assert payload1.equals(new String[] {"member1", "member2", "member3"}); // Returns all members in ascending order.
            *
            * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
            * String[] payload2 = client.zrange("mySortedSet", query2).get();
      -     * assert payload2.equals(new String[] {'member2', 'member3'}); // Returns members with scores within the range of negative infinity to 3, in ascending order.
      +     * assert payload2.equals(new String[] {"member2", "member3"}); // Returns members with scores within the range of negative infinity to 3, in ascending order.
            * }
      */ CompletableFuture zrange(String key, RangeQuery rangeQuery); + /** + * Returns the specified range of elements in the sorted set stored at key.
      + * ZRANGE can perform different types of range queries: by index (rank), by the + * score, or by lexicographical order.
      + * To get the elements with their scores, see {@link #zrangeWithScores}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
      + *
        + *
      • For range queries by index (rank), use {@link RangeByIndex}. + *
      • For range queries by lexicographical order, use {@link RangeByLex}. + *
      • For range queries by score, use {@link RangeByScore}. + *
      + * + * @return An of array elements within the specified range. If key does not exist, it + * is treated as an empty sorted set, and the command returns an empty array. + * @example + *
      {@code
      +     * RangeByIndex query1 = new RangeByIndex(0, -1);
      +     * GlideString[] payload1 = client.zrange(gs("mySortedSet"),query1).get();
      +     * assert payload1.equals(new GlideString[] {gs("member1")
      +     * , gs("member2")
      +     * , gs("member3")
      +     * }); // Returns all members in ascending order.
      +     *
      +     * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
      +     * GlideString[] payload2 = client.zrange(gs("mySortedSet"), query2).get();
      +     * assert Arrays.equals(payload2, new GlideString[] {gs("member2"), gs("member3")}); // Returns members with scores within the range of negative infinity to 3, in ascending order.
      +     * }
      + */ + CompletableFuture zrange(GlideString key, RangeQuery rangeQuery); + /** * Returns the specified range of elements with their scores in the sorted set stored at key * . Similar to {@link #zrange} but with a WITHSCORE flag. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
      *
        @@ -381,11 +866,11 @@ CompletableFuture zaddIncr( *
        {@code
              * RangeByScore query1 = new RangeByScore(new ScoreBoundary(10), new ScoreBoundary(20));
              * Map payload1 = client.zrangeWithScores("mySortedSet", query1, true).get();
        -     * assert payload1.equals(Map.of('member2', 15.2, 'member1', 10.5)); // Returns members with scores between 10 and 20 (inclusive) with their scores.
        +     * assert payload1.equals(Map.of("member2", 15.2, "member1", 10.5)); // Returns members with scores between 10 and 20 (inclusive) with their scores.
              *
              * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
              * Map payload2 = client.zrangeWithScores("mySortedSet", query2, false).get();
        -     * assert payload2.equals(Map.of('member4', -2.0, 'member7', 1.5)); // Returns members with with scores within the range of negative infinity to 3, with their scores.
        +     * assert payload2.equals(Map.of("member4", -2.0, "member7", 1.5)); // Returns members with with scores within the range of negative infinity to 3, with their scores.
              * }
        */ CompletableFuture> zrangeWithScores( @@ -395,7 +880,38 @@ CompletableFuture> zrangeWithScores( * Returns the specified range of elements with their scores in the sorted set stored at key * . Similar to {@link #zrange} but with a WITHSCORE flag. * - * @see redis.io for more details. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
        + *
          + *
        • For range queries by index (rank), use {@link RangeByIndex}. + *
        • For range queries by score, use {@link RangeByScore}. + *
        + * + * @param reverse If true, reverses the sorted set, with index 0 as the element with the highest + * score. + * @return A Map of elements and their scores within the specified range. If + * key does not exist, it is treated as an empty sorted set, and the command returns an + * empty Map. + * @example + *
        {@code
        +     * RangeByScore query1 = new RangeByScore(new ScoreBoundary(10), new ScoreBoundary(20));
        +     * Map payload1 = client.zrangeWithScores(gs("mySortedSet"), query1, true).get();
        +     * assert payload1.equals(Map.of(gs("member2"), 15.2, gs("member1"), 10.5)); // Returns members with scores between 10 and 20 (inclusive) with their scores.
        +     *
        +     * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
        +     * Map payload2 = client.zrangeWithScores(gs("mySortedSet"), query2, false).get();
        +     * assert payload2.equals(Map.of(gs("member4"), -2.0, gs("member7"), 1.5)); // Returns members with with scores within the range of negative infinity to 3, with their scores.
        +     * }
        + */ + CompletableFuture> zrangeWithScores( + GlideString key, ScoredRangeQuery rangeQuery, boolean reverse); + + /** + * Returns the specified range of elements with their scores in the sorted set stored at key + * . Similar to {@link #zrange} but with a WITHSCORE flag. + * + * @see valkey.io for more details. * @param key The key of the sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
        *
          @@ -410,21 +926,52 @@ CompletableFuture> zrangeWithScores( *
          {@code
                * RangeByScore query1 = new RangeByScore(new ScoreBoundary(10), new ScoreBoundary(20));
                * Map payload1 = client.zrangeWithScores("mySortedSet", query1).get();
          -     * assert payload1.equals(Map.of('member1', 10.5, 'member2', 15.2)); // Returns members with scores between 10 and 20 (inclusive) with their scores.
          +     * assert payload1.equals(Map.of("member1", 10.5, "member2", 15.2)); // Returns members with scores between 10 and 20 (inclusive) with their scores.
                *
                * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
                * Map payload2 = client.zrangeWithScores("mySortedSet", query2).get();
          -     * assert payload2.equals(Map.of('member4', -2.0, 'member7', 1.5)); // Returns members with with scores within the range of negative infinity to 3, with their scores.
          +     * assert payload2.equals(Map.of("member4", -2.0, "member7", 1.5)); // Returns members with with scores within the range of negative infinity to 3, with their scores.
                * }
          */ CompletableFuture> zrangeWithScores(String key, ScoredRangeQuery rangeQuery); + /** + * Returns the specified range of elements with their scores in the sorted set stored at key + * . Similar to {@link #zrange} but with a WITHSCORE flag. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
          + *
            + *
          • For range queries by index (rank), use {@link RangeByIndex}. + *
          • For range queries by score, use {@link RangeByScore}. + *
          + * + * @return A Map of elements and their scores within the specified range. If + * key does not exist, it is treated as an empty sorted set, and the command returns an + * empty Map. + * @example + *
          {@code
          +     * RangeByScore query1 = new RangeByScore(new ScoreBoundary(10), new ScoreBoundary(20));
          +     * Map payload1 = client.zrangeWithScores(gs("mySortedSet"), query1).get();
          +     * assert payload1.equals(Map.of(gs("member1"), 10.5, gs("member2"), 15.2)); // Returns members with scores between 10 and 20 (inclusive) with their scores.
          +     *
          +     * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3));
          +     * Map payload2 = client.zrangeWithScores(gs("mySortedSet"), query2).get();
          +     * assert payload2.equals(Map.of(gs("member4"), -2.0, gs("member7"), 1.5)); // Returns members with with scores within the range of negative infinity to 3, with their scores.
          +     * }
          + */ + CompletableFuture> zrangeWithScores( + GlideString key, ScoredRangeQuery rangeQuery); + /** * Stores a specified range of elements from the sorted set at source, into a new * sorted set at destination. If destination doesn't exist, a new sorted * set is created; if it exists, it's overwritten.
          * - * @see redis.io for more details. + * @apiNote When in cluster mode, destination and source must map to the + * same hash slot. + * @see valkey.io for more details. * @param destination The key for the destination sorted set. * @param source The key of the source sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
          @@ -456,7 +1003,43 @@ CompletableFuture zrangestore( * sorted set at destination. If destination doesn't exist, a new sorted * set is created; if it exists, it's overwritten.
          * - * @see redis.io for more details. + * @apiNote When in cluster mode, destination and source must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key for the destination sorted set. + * @param source The key of the source sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
          + *
            + *
          • For range queries by index (rank), use {@link RangeByIndex}. + *
          • For range queries by lexicographical order, use {@link RangeByLex}. + *
          • For range queries by score, use {@link RangeByScore}. + *
          + * + * @param reverse If true, reverses the sorted set, with index 0 as the + * element with the highest score. + * @return The number of elements in the resulting sorted set. + * @example + *
          {@code
          +     * RangeByIndex query1 = new RangeByIndex(0, -1); // Query for all members.
          +     * Long payload1 = client.zrangestore(gs("destinationKey"), gs("mySortedSet"), query1, true).get();
          +     * assert payload1 == 7L;
          +     *
          +     * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3)); // Query for members with scores within the range of negative infinity to 3.
          +     * Long payload2 = client.zrangestore(gs("destinationKey"), gs("mySortedSet"), query2, false).get();
          +     * assert payload2 == 5L;
          +     * }
          + */ + CompletableFuture zrangestore( + GlideString destination, GlideString source, RangeQuery rangeQuery, boolean reverse); + + /** + * Stores a specified range of elements from the sorted set at source, into a new + * sorted set at destination. If destination doesn't exist, a new sorted + * set is created; if it exists, it's overwritten.
          + * + * @apiNote When in cluster mode, destination and source must map to the + * same hash slot. + * @see valkey.io for more details. * @param destination The key for the destination sorted set. * @param source The key of the source sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
          @@ -480,12 +1063,44 @@ CompletableFuture zrangestore( */ CompletableFuture zrangestore(String destination, String source, RangeQuery rangeQuery); + /** + * Stores a specified range of elements from the sorted set at source, into a new + * sorted set at destination. If destination doesn't exist, a new sorted + * set is created; if it exists, it's overwritten.
          + * + * @apiNote When in cluster mode, destination and source must map to the + * same hash slot. + * @see valkey.io for more details. + * @param destination The key for the destination sorted set. + * @param source The key of the source sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
          + *
            + *
          • For range queries by index (rank), use {@link RangeByIndex}. + *
          • For range queries by lexicographical order, use {@link RangeByLex}. + *
          • For range queries by score, use {@link RangeByScore}. + *
          + * + * @return The number of elements in the resulting sorted set. + * @example + *
          {@code
          +     * RangeByIndex query1 = new RangeByIndex(0, -1); // Query for all members.
          +     * Long payload1 = client.zrangestore(gs("destinationKey"), gs("mySortedSet"), query1).get();
          +     * assert payload1 == 7L;
          +     *
          +     * RangeByScore query2 = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3)); // Query for members with scores within the range of negative infinity to 3.
          +     * Long payload2 = client.zrangestore(gs("destinationKey"), gs("mySortedSet"), query2).get();
          +     * assert payload2 == 5L;
          +     * }
          + */ + CompletableFuture zrangestore( + GlideString destination, GlideString source, RangeQuery rangeQuery); + /** * Returns the rank of member in the sorted set stored at key, with - * scores ordered from low to high.
          + * scores ordered from low to high, starting from 0.
          * To get the rank of member with its score, see {@link #zrankWithScore}. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member The member whose rank is to be retrieved. * @return The rank of member in the sorted set.
          @@ -502,11 +1117,33 @@ CompletableFuture zrangestore( */ CompletableFuture zrank(String key, String member); + /** + * Returns the rank of member in the sorted set stored at key, with + * scores ordered from low to high, starting from 0.
          + * To get the rank of member with its score, see {@link #zrankWithScore}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return The rank of member in the sorted set.
          + * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
          {@code
          +     * Long num1 = client.zrank(gs("mySortedSet"), gs("member2")).get();
          +     * assert num1 == 3L; // Indicates that "member2" has the second-lowest score in the sorted set "mySortedSet".
          +     *
          +     * Long num2 = client.zcard(gs("mySortedSet"), gs("nonExistingMember")).get();
          +     * assert num2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
          +     * }
          + */ + CompletableFuture zrank(GlideString key, GlideString member); + /** * Returns the rank of member in the sorted set stored at key with its - * score, where scores are ordered from the lowest to highest. + * score, where scores are ordered from the lowest to highest, starting from 0.
          * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member The member whose rank is to be retrieved. * @return An array containing the rank (as Long) and score (as Double) @@ -516,19 +1153,111 @@ CompletableFuture zrangestore( * @example *
          {@code
                * Object[] result1 = client.zrankWithScore("mySortedSet", "member2").get();
          -     * assert ((Long)result1[0]) == 1L && ((Double)result1[1]) == 6.0; // Indicates that "member2" with score 6.0 has the second-lowest score in the sorted set "mySortedSet".
          +     * assert ((Long) result1[0]) == 1L && ((Double) result1[1]) == 6.0; // Indicates that "member2" with score 6.0 has the second-lowest score in the sorted set "mySortedSet".
                *
                * Object[] result2 = client.zrankWithScore("mySortedSet", "nonExistingMember").get();
          -     * assert num2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
          +     * assert result2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
                * }
          */ CompletableFuture zrankWithScore(String key, String member); + /** + * Returns the rank of member in the sorted set stored at key, where + * scores are ordered from the highest to lowest, starting from 0.
          + * To get the rank of member with its score, see {@link #zrevrankWithScore}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return The rank of member in the sorted set, where ranks are ordered from high to + * low based on scores.
          + * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
          {@code
          +     * Long num1 = client.zrevrank("mySortedSet", "member2").get();
          +     * assert num1 == 1L; // Indicates that "member2" has the second-highest score in the sorted set "mySortedSet".
          +     *
          +     * Long num2 = client.zrevrank("mySortedSet", "nonExistingMember").get();
          +     * assert num2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
          +     * }
          + */ + CompletableFuture zrevrank(String key, String member); + + /** + * Returns the rank of member in the sorted set stored at key, where + * scores are ordered from the highest to lowest, starting from 0.
          + * To get the rank of member with its score, see {@link #zrevrankWithScore}. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return The rank of member in the sorted set, where ranks are ordered from high to + * low based on scores.
          + * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
          {@code
          +     * Long num1 = client.zrevrank(gs("mySortedSet"), gs("member2")).get();
          +     * assert num1 == 1L; // Indicates that "member2" has the second-highest score in the sorted set "mySortedSet".
          +     *
          +     * Long num2 = client.zrevrank(gs("mySortedSet"), gs("nonExistingMember")).get();
          +     * assert num2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
          +     * }
          + */ + CompletableFuture zrevrank(GlideString key, GlideString member); + + /** + * Returns the rank of member in the sorted set stored at key with its + * score, where scores are ordered from the highest to lowest, starting from 0. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return An array containing the rank (as Long) and score (as Double) + * of member in the sorted set, where ranks are ordered from high to low based on + * scores.
          + * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
          {@code
          +     * Object[] result1 = client.zrevrankWithScore("mySortedSet", "member2").get();
          +     * assert ((Long) result1[0]) == 1L && ((Double) result1[1]) == 6.0; // Indicates that "member2" with score 6.0 has the second-highest score in the sorted set "mySortedSet".
          +     *
          +     * Object[] result2 = client.zrevrankWithScore("mySortedSet", "nonExistingMember").get();
          +     * assert result2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
          +     * }
          + */ + CompletableFuture zrevrankWithScore(String key, String member); + + /** + * Returns the rank of member in the sorted set stored at key with its + * score, where scores are ordered from the highest to lowest, starting from 0. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return An array containing the rank (as Long) and score (as Double) + * of member in the sorted set, where ranks are ordered from high to low based on + * scores.
          + * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
          {@code
          +     * Object[] result1 = client.zrevrankWithScore(gs("mySortedSet"), gs("member2")).get();
          +     * assert ((Long) result1[0]) == 1L && ((Double) result1[1]) == 6.0; // Indicates that "member2" with score 6.0 has the second-highest score in the sorted set "mySortedSet".
          +     *
          +     * Object[] result2 = client.zrevrankWithScore(gs("mySortedSet"), gs("nonExistingMember")).get();
          +     * assert result2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
          +     * }
          + */ + CompletableFuture zrevrankWithScore(GlideString key, GlideString member); + /** * Returns the scores associated with the specified members in the sorted set stored * at key. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param members An array of members in the sorted set. * @return An Array of scores of the members.
          @@ -542,11 +1271,31 @@ CompletableFuture zrangestore( */ CompletableFuture zmscore(String key, String[] members); + /** + * Returns the scores associated with the specified members in the sorted set stored + * at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members An array of members in the sorted set. + * @return An Array of scores of the members.
          + * If a member does not exist, the corresponding value in the Array + * will be null. + * @example + *
          {@code
          +     * Double[] payload = client.zmscore(key1, new GlideString[] {gs("one"), gs("nonExistentMember"), gs("three")}).get();
          +     * assert payload.equals(new Double[] {1.0, null, 3.0});
          +     * }
          + */ + CompletableFuture zmscore(GlideString key, GlideString[] members); + /** * Returns the difference between the first sorted set and all the successive sorted sets.
          * To get the elements with their scores, see {@link #zdiffWithScores}. * - * @see redis.io for more details. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. * @param keys The keys of the sorted sets. * @return An array of elements representing the difference between the sorted sets. *
          @@ -560,10 +1309,32 @@ CompletableFuture zrangestore( */ CompletableFuture zdiff(String[] keys); + /** + * Returns the difference between the first sorted set and all the successive sorted sets.
          + * To get the elements with their scores, see {@link #zdiffWithScores}. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return An array of elements representing the difference between the sorted sets. + *
          + * If the first key does not exist, it is treated as an empty sorted set, and the + * command returns an empty array. + * @example + *
          {@code
          +     * GlideString[] payload = client.zdiff(new GlideString[] {gs("sortedSet1"), gs("sortedSet2"), gs("sortedSet3")}).get();
          +     * assert payload.equals(new GlideString[]{gs("element1")});
          +     * }
          + */ + CompletableFuture zdiff(GlideString[] keys); + /** * Returns the difference between the first sorted set and all the successive sorted sets. * - * @see redis.io for more details. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. * @param keys The keys of the sorted sets. * @return A Map of elements and their scores representing the difference between the * sorted sets.
          @@ -577,12 +1348,34 @@ CompletableFuture zrangestore( */ CompletableFuture> zdiffWithScores(String[] keys); + /** + * Returns the difference between the first sorted set and all the successive sorted sets. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return A Map of elements and their scores representing the difference between the + * sorted sets.
          + * If the first key does not exist, it is treated as an empty sorted set, and the + * command returns an empty Map. + * @example + *
          {@code
          +     * Map payload = client.zdiffWithScores(new GlideString[] {gs("sortedSet1"), gs("sortedSet2"), gs("sortedSet3")}).get();
          +     * assert payload.equals(Map.of(gs("element1"), 1.0));
          +     * }
          + */ + CompletableFuture> zdiffWithScores(GlideString[] keys); + /** * Calculates the difference between the first sorted set and all the successive sorted sets at * keys and stores the difference as a sorted set to destination, * overwriting it if it already exists. Non-existent keys are treated as empty sets. * - * @see redis.io for more details. + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. * @param destination The key for the resulting sorted set. * @param keys The keys of the sorted sets to compare. * @return The number of members in the resulting sorted set stored at destination. @@ -594,11 +1387,31 @@ CompletableFuture zrangestore( */ CompletableFuture zdiffstore(String destination, String[] keys); + /** + * Calculates the difference between the first sorted set and all the successive sorted sets at + * keys and stores the difference as a sorted set to destination, + * overwriting it if it already exists. Non-existent keys are treated as empty sets. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param destination The key for the resulting sorted set. + * @param keys The keys of the sorted sets to compare. + * @return The number of members in the resulting sorted set stored at destination. + * @example + *
          {@code
          +     * Long payload = client.zdiffstore(gs("mySortedSet"), new GlideString[] {gs("key1"), gs("key2")}).get();
          +     * assert payload > 0; // At least one member differed in "key1" compared to "key2", and this difference was stored in "mySortedSet".
          +     * }
          + */ + CompletableFuture zdiffstore(GlideString destination, GlideString[] keys); + /** * Returns the number of members in the sorted set stored at key with scores between * minScore and maxScore. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minScore The minimum score to count from. Can be an implementation of {@link * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} @@ -621,13 +1434,40 @@ CompletableFuture zrangestore( */ CompletableFuture zcount(String key, ScoreRange minScore, ScoreRange maxScore); + /** + * Returns the number of members in the sorted set stored at key with scores between + * minScore and maxScore. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param minScore The minimum score to count from. Can be an implementation of {@link + * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} + * representing a specific score and inclusivity. + * @param maxScore The maximum score to count up to. Can be an implementation of {@link + * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} + * representing a specific score and inclusivity. + * @return The number of members in the specified score range.
          + * If key does not exist, it is treated as an empty sorted set, and the command + * returns 0.
          + * If maxScore < minScore, 0 is returned. + * @example + *
          {@code
          +     * Long num1 = client.zcount(gs("my_sorted_set"), new ScoreBoundary(5.0, true), InfScoreBound.POSITIVE_INFINITY).get();
          +     * assert num1 == 2L; // Indicates that there are 2 members with scores between 5.0 (inclusive) and +inf in the sorted set "my_sorted_set".
          +     *
          +     * Long num2 = client.zcount(gs("my_sorted_set"), new ScoreBoundary(5.0, true), new ScoreBoundary(10.0, false)).get();
          +     * assert num2 == 1L; // Indicates that there is one member with ScoreBoundary 5.0 <= score < 10.0 in the sorted set "my_sorted_set".
          +     * }
          + */ + CompletableFuture zcount(GlideString key, ScoreRange minScore, ScoreRange maxScore); + /** * Removes all elements in the sorted set stored at key with rank between start * and end. Both start and end are zero-based * indexes with 0 being the element with the lowest score. These indexes can be * negative numbers, where they indicate offsets starting at the element with the highest score. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param start The starting point of the range. * @param end The end of the range. @@ -648,11 +1488,38 @@ CompletableFuture zrangestore( */ CompletableFuture zremrangebyrank(String key, long start, long end); + /** + * Removes all elements in the sorted set stored at key with rank between start + * and end. Both start and end are zero-based + * indexes with 0 being the element with the lowest score. These indexes can be + * negative numbers, where they indicate offsets starting at the element with the highest score. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param start The starting point of the range. + * @param end The end of the range. + * @return The number of elements removed.
          + * If start exceeds the end of the sorted set, or if start is + * greater than end, 0 returned.
          + * If end exceeds the actual end of the sorted set, the range will stop at the + * actual end of the sorted set.
          + * If key does not exist 0 will be returned. + * @example + *
          {@code
          +     * Long payload1 = client.zremrangebyrank(gs("mySortedSet"), 0, 4).get();
          +     * assert payload1 == 5L; // Indicates that 5 elements, with ranks ranging from 0 to 4 (inclusive), have been removed from "mySortedSet".
          +     *
          +     * Long payload2 = client.zremrangebyrank(gs("mySortedSet"), 0, 4).get();
          +     * assert payload2 == 0L; // Indicates that nothing was removed.
          +     * }
          + */ + CompletableFuture zremrangebyrank(GlideString key, long start, long end); + /** * Removes all elements in the sorted set stored at key with a lexicographical order * between minLex and maxLex. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minLex The minimum bound of the lexicographical range. Can be an implementation of * {@link InfLexBound} representing positive/negative infinity, or {@link LexBoundary} @@ -675,11 +1542,38 @@ CompletableFuture zrangestore( */ CompletableFuture zremrangebylex(String key, LexRange minLex, LexRange maxLex); + /** + * Removes all elements in the sorted set stored at key with a lexicographical order + * between minLex and maxLex. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param minLex The minimum bound of the lexicographical range. Can be an implementation of + * {@link InfLexBound} representing positive/negative infinity, or {@link LexBoundary} + * representing a specific lex and inclusivity. + * @param maxLex The maximum bound of the lexicographical range. Can be an implementation of + * {@link InfLexBound} representing positive/negative infinity, or {@link LexBoundary} + * representing a specific lex and inclusivity. + * @return The number of members removed from the sorted set.
          + * If key does not exist, it is treated as an empty sorted set, and the command + * returns 0.
          + * If minLex is greater than maxLex, 0 is returned. + * @example + *
          {@code
          +     * Long payload1 = client.zremrangebylex(gs("mySortedSet"), new LexBoundary("a", false), new LexBoundary("e")).get();
          +     * assert payload1 == 4L; // Indicates that 4 members, with lexicographical values ranging from "a" (exclusive) to "e" (inclusive), have been removed from "mySortedSet".
          +     *
          +     * Long payload2 = client.zremrangebylex(gs("mySortedSet"), InfLexBound.NEGATIVE_INFINITY, new LexBoundary("e")).get();
          +     * assert payload2 == 0L; // Indicates that no elements were removed.
          +     * }
          + */ + CompletableFuture zremrangebylex(GlideString key, LexRange minLex, LexRange maxLex); + /** * Removes all elements in the sorted set stored at key with a score between * minScore and maxScore. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minScore The minimum score to remove from. Can be an implementation of {@link * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} @@ -702,11 +1596,39 @@ CompletableFuture zrangestore( */ CompletableFuture zremrangebyscore(String key, ScoreRange minScore, ScoreRange maxScore); + /** + * Removes all elements in the sorted set stored at key with a score between + * minScore and maxScore. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param minScore The minimum score to remove from. Can be an implementation of {@link + * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} + * representing a specific score and inclusivity. + * @param maxScore The maximum score to remove to. Can be an implementation of {@link + * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} + * representing a specific score and inclusivity. + * @return The number of members removed.
          + * If key does not exist, it is treated as an empty sorted set, and the command + * returns 0.
          + * If minScore is greater than maxScore, 0 is returned. + * @example + *
          {@code
          +     * Long payload1 = client.zremrangebyscore(gs("mySortedSet"), new ScoreBoundary(1, false), new ScoreBoundary(5)).get();
          +     * assert payload1 == 4L; // Indicates that 4 members, with scores ranging from 1 (exclusive) to 5 (inclusive), have been removed from "mySortedSet".
          +     *
          +     * Long payload2 = client.zremrangebyscore(gs("mySortedSet"), InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(-42)).get();
          +     * assert payload2 == 0L; // Indicates that no elements were removed.
          +     * }
          + */ + CompletableFuture zremrangebyscore( + GlideString key, ScoreRange minScore, ScoreRange maxScore); + /** * Returns the number of members in the sorted set stored at key with scores between * minLex and maxLex. * - * @see redis.io for more details. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minLex The minimum lex to count from. Can be an implementation of {@link InfLexBound} * representing positive/negative infinity, or {@link LexBoundary} representing a specific lex @@ -728,4 +1650,1204 @@ CompletableFuture zrangestore( * }
*/ CompletableFuture zlexcount(String key, LexRange minLex, LexRange maxLex); + + /** + * Returns the number of members in the sorted set stored at key with scores between + * minLex and maxLex. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param minLex The minimum lex to count from. Can be an implementation of {@link InfLexBound} + * representing positive/negative infinity, or {@link LexBoundary} representing a specific lex + * and inclusivity. + * @param maxLex The maximum lex to count up to. Can be an implementation of {@link InfLexBound} + * representing positive/negative infinity, or {@link LexBoundary} representing a specific lex + * and inclusivity. + * @return The number of members in the specified lex range.
+ * If key does not exist, it is treated as an empty sorted set, and the command + * returns 0.
+ * If maxLex < minLex, 0 is returned. + * @example + *
{@code
+     * Long num1 = client.zlexcount(gs("my_sorted_set"), new LexBoundary(gs("c"), true), InfLexBound.POSITIVE_INFINITY).get();
+     * assert num1 == 2L; // Indicates that there are 2 members with lex scores between "c" (inclusive) and positive infinity in the sorted set "my_sorted_set".
+     *
+     * Long num2 = client.zlexcount(gs("my_sorted_set"), new ScoreBoundary(gs("c"), true), new ScoreBoundary(gs("k"), false)).get();
+     * assert num2 == 1L; // Indicates that there is one member with LexBoundary "c" <= score < "k" in the sorted set "my_sorted_set".
+     * }
+ */ + CompletableFuture zlexcount(GlideString key, LexRange minLex, LexRange maxLex); + + /** + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 1.0), Pair.of("mySortedSet2", 2.0)));
+     * Long payload = client.zunionstore("newSortedSet", weightedKeys, Aggregate.MAX).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the union of "mySortedSet1" and "mySortedSet2".
+     * }
+ */ + CompletableFuture zunionstore( + String destination, KeysOrWeightedKeys keysOrWeightedKeys, Aggregate aggregate); + + /** + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * WeightedKeysBinary weightedKeys = new WeightedKeys(List.of(Pair.of(gs("mySortedSet1"), 1.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * Long payload = client.zunionstore(gs("newSortedSet"), weightedKeys, Aggregate.MAX).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the union of gs("mySortedSet1") and gs("mySortedSet2").
+     * }
+ */ + CompletableFuture zunionstore( + GlideString destination, KeysOrWeightedKeysBinary keysOrWeightedKeys, Aggregate aggregate); + + /** + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * Long payload = client.zunionstore("newSortedSet", keyArray).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the union of "mySortedSet1" and "mySortedSet2".
+     * }
+ */ + CompletableFuture zunionstore(String destination, KeysOrWeightedKeys keysOrWeightedKeys); + + /** + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * Long payload = client.zunionstore(gs("newSortedSet"), keyArray).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the union of gs("mySortedSet1") and gs("mySortedSet2").
+     * }
+ */ + CompletableFuture zunionstore( + GlideString destination, KeysOrWeightedKeysBinary keysOrWeightedKeys); + + /** + * Computes the intersection of sorted sets given by the specified keysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 1.0), Pair.of("mySortedSet2", 2.0)));
+     * Long payload = client.zinterstore("newSortedSet", weightedKeys, Aggregate.MAX).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the intersection of "mySortedSet1" and "mySortedSet2".
+     * }
+ */ + CompletableFuture zinterstore( + String destination, KeysOrWeightedKeys keysOrWeightedKeys, Aggregate aggregate); + + /** + * Computes the intersection of sorted sets given by the specified keysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * WeightedKeysBinary weightedKeys = new WeightedKeysBinary(List.of(Pair.of(gs("mySortedSet1"), 1.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * Long payload = client.zinterstore(gs("newSortedSet"), weightedKeys, Aggregate.MAX).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the intersection of "mySortedSet1" and "mySortedSet2".
+     * }
+ */ + CompletableFuture zinterstore( + GlideString destination, KeysOrWeightedKeysBinary keysOrWeightedKeys, Aggregate aggregate); + + /** + * Computes the intersection of sorted sets given by the specified keysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * Long payload = client.zinterstore("newSortedSet", keyArray).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the intersection of "mySortedSet1" and "mySortedSet2".
+     * }
+ */ + CompletableFuture zinterstore(String destination, KeysOrWeightedKeys keysOrWeightedKeys); + + /** + * Computes the intersection of sorted sets given by the specified keysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * @apiNote When in cluster mode, destination and all keys in + * keysOrWeightedKeys must map to the same hash slot. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The number of elements in the resulting sorted set stored at destination. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArray(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * Long payload = client.zinterstore(gs("newSortedSet"), keyArray).get()
+     * assert payload == 3L; // Indicates the new sorted set contains three members from the intersection of "mySortedSet1" and "mySortedSet2".
+     * }
+ */ + CompletableFuture zinterstore( + GlideString destination, KeysOrWeightedKeysBinary keysOrWeightedKeys); + + /** + * Pops a member-score pair from the first non-empty sorted set, with the given keys + * being checked in the order they are provided. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop the member with the lowest/highest score accordingly. + * @return A two-element array containing the key name of the set from which the + * element was popped, and a member-score Map of the popped element.
+ * If no member could be popped, returns null. + * @example + *
{@code
+     * Object[] result = client.zmpop(new String[] { "zSet1", "zSet2" }, MAX).get();
+     * Map data = (Map)result[1];
+     * String element = data.keySet().toArray(String[]::new)[0];
+     * System.out.printf("Popped '%s' with score %d from '%s'%n", element, data.get(element), result[0]);
+     * }
+ */ + CompletableFuture zmpop(String[] keys, ScoreFilter modifier); + + /** + * Pops a member-score pair from the first non-empty sorted set, with the given keys + * being checked in the order they are provided. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop the member with the lowest/highest score accordingly. + * @return A two-element array containing the key name of the set from which the + * element was popped, and a member-score Map of the popped element.
+ * If no member could be popped, returns null. + * @example + *
{@code
+     * Object[] result = client.zmpop(new GlideString[] { gs("zSet1"), gs("zSet2") }, MAX).get();
+     * Map data = (Map)result[1];
+     * GlideString element = data.keySet().toArray(GlideString[]::new)[0];
+     * System.out.printf("Popped '%s' with score %d from '%s'%n", element, data.get(element), result[0]);
+     * }
+ */ + CompletableFuture zmpop(GlideString[] keys, ScoreFilter modifier); + + /** + * Pops multiple member-score pairs from the first non-empty sorted set, with the given keys + * being checked in the order they are provided. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param count The number of elements to pop. + * @return A two-element array containing the key name of the set from which elements + * were popped, and a member-score Map of the popped elements.
+ * If no member could be popped, returns null. + * @example + *
{@code
+     * Object[] result = client.zmpop(new String[] { "zSet1", "zSet2" }, MAX, 2).get();
+     * Map data = (Map)result[1];
+     * for (Map.Entry entry : data.entrySet()) {
+     *     System.out.printf("Popped '%s' with score %d from '%s'%n", entry.getKey(), entry.getValue(), result[0]);
+     * }
+     * }
+ */ + CompletableFuture zmpop(String[] keys, ScoreFilter modifier, long count); + + /** + * Pops multiple member-score pairs from the first non-empty sorted set, with the given keys + * being checked in the order they are provided. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param count The number of elements to pop. + * @return A two-element array containing the key name of the set from which elements + * were popped, and a member-score Map of the popped elements.
+ * If no member could be popped, returns null. + * @example + *
{@code
+     * Object[] result = client.zmpop(new GlideString[] { gs("zSet1"), gs("zSet2") }, MAX, 2).get();
+     * Map data = (Map)result[1];
+     * for (Map.Entry entry : data.entrySet()) {
+     *     System.out.printf("Popped '%s' with score %d from '%s'%n", entry.getKey(), entry.getValue(), result[0]);
+     * }
+     * }
+ */ + CompletableFuture zmpop(GlideString[] keys, ScoreFilter modifier, long count); + + /** + * Blocks the connection until it pops and returns a member-score pair from the first non-empty + * sorted set, with the given keys being checked in the order they are provided.
+ * BZMPOP is the blocking variant of {@link #zmpop(String[], ScoreFilter)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BZMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key name of the set from which an + * element was popped, and a member-score Map of the popped elements.
+ * If no member could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * Object[] result = client.bzmpop(new String[] { "zSet1", "zSet2" }, MAX, 0.1).get();
+     * Map data = (Map)result[1];
+     * String element = data.keySet().toArray(String[]::new)[0];
+     * System.out.printf("Popped '%s' with score %d from '%s'%n", element, data.get(element), result[0]);
+     * }
+ */ + CompletableFuture bzmpop(String[] keys, ScoreFilter modifier, double timeout); + + /** + * Blocks the connection until it pops and returns a member-score pair from the first non-empty + * sorted set, with the given keys being checked in the order they are provided.
+ * BZMPOP is the blocking variant of {@link #zmpop(String[], ScoreFilter)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BZMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return A two-element array containing the key name of the set from which an + * element was popped, and a member-score Map of the popped elements.
+ * If no member could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * Object[] result = client.bzmpop(new GlideString[] { gs("zSet1"), gs("zSet2") }, MAX, 0.1).get();
+     * Map data = (Map)result[1];
+     * GlideString element = data.keySet().toArray(GlideString[]::new)[0];
+     * System.out.printf("Popped '%s' with score %d from '%s'%n", element, data.get(element), result[0]);
+     * }
+ */ + CompletableFuture bzmpop(GlideString[] keys, ScoreFilter modifier, double timeout); + + /** + * Blocks the connection until it pops and returns multiple member-score pairs from the first + * non-empty sorted set, with the given keys being checked in the order they are + * provided.
+ * BZMPOP is the blocking variant of {@link #zmpop(String[], ScoreFilter, long)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BZMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @param count The number of elements to pop. + * @return A two-element array containing the key name of the set from which elements + * were popped, and a member-score Map of the popped elements.
+ * If no members could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * Object[] result = client.bzmpop(new String[] { "zSet1", "zSet2" }, MAX, 0.1, 2).get();
+     * Map data = (Map)result[1];
+     * for (Map.Entry entry : data.entrySet()) {
+     *     System.out.printf("Popped '%s' with score %d from '%s'%n", entry.getKey(), entry.getValue(), result[0]);
+     * }
+     * }
+ */ + CompletableFuture bzmpop( + String[] keys, ScoreFilter modifier, double timeout, long count); + + /** + * Blocks the connection until it pops and returns multiple member-score pairs from the first + * non-empty sorted set, with the given keys being checked in the order they are + * provided.
+ * BZMPOP is the blocking variant of {@link #zmpop(String[], ScoreFilter, long)}. + * + * @apiNote + *
    + *
  1. When in cluster mode, all keys must map to the same hash slot. + *
  2. BZMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + *
+ * + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @param count The number of elements to pop. + * @return A two-element array containing the key name of the set from which elements + * were popped, and a member-score Map of the popped elements.
+ * If no members could be popped and the timeout expired, returns null. + * @example + *
{@code
+     * Object[] result = client.bzmpop(new GlideString[] { gs("zSet1"), gs("zSet2") }, MAX, 0.1, 2).get();
+     * Map data = (Map)result[1];
+     * for (Map.Entry entry : data.entrySet()) {
+     *     System.out.printf("Popped '%s' with score %d from '%s'%n", entry.getKey(), entry.getValue(), result[0]);
+     * }
+     * }
+ */ + CompletableFuture bzmpop( + GlideString[] keys, ScoreFilter modifier, double timeout, long count); + + /** + * Returns the union of members from sorted sets specified by the given keys.
+ * To get the elements with their scores, see {@link #zunionWithScores}. + * + * @apiNote When in cluster mode, all keys in keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return The resulting sorted set from the union. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * String[] payload1 = client.zunion(keyArray).get()
+     * assert Arrays.equals(payload1, new String[] {"elem1", "elem2", "elem3"});
+     *
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 2.0), Pair.of("mySortedSet2", 2.0)));
+     * String[] payload2 = client.zunion(weightedKeys).get()
+     * assert Arrays.equals(payload2, new String[] {"elem1", "elem2", "elem3"});
+     * }
+ */ + CompletableFuture zunion(KeyArray keys); + + /** + * Returns the union of members from sorted sets specified by the given keys.
+ * To get the elements with their scores, see {@link #zunionWithScores}. + * + * @apiNote When in cluster mode, all keys in keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return The resulting sorted set from the union. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * GlideString[] payload1 = client.zunion(keyArray).get()
+     * assert Arrays.equals(payload1, new GlideString[] {gs("elem1"), gs("elem2"), gs("elem3")});
+     *
+     * WeightedKeysBinary weightedKeys = new WeightedKeysBinary(List.of(Pair.of(gs("mySortedSet1"), 2.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * GlideString[] payload2 = client.zunion(weightedKeys).get()
+     * assert Arrays.equals(payload2, new GlideString[] {gs("elem1"), gs("elem2"), gs("elem3")});
+     * }
+ */ + CompletableFuture zunion(KeyArrayBinary keys); + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The resulting sorted set from the union. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * Map payload1 = client.zunionWithScores(keyArray, Aggregate.MAX).get();
+     * assert Arrays.equals(payload1, Map.of("elem1", 1.0, "elem2", 2.0, "elem3", 3.0));
+     *
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 2.0), Pair.of("mySortedSet2", 2.0)));
+     * Map payload2 = client.zunionWithScores(keyArray, Aggregate.SUM).get();
+     * assert Arrays.equals(payload2, Map.of("elem1", 2.0, "elem2", 4.0, "elem3", 6.0));
+     * }
+ */ + CompletableFuture> zunionWithScores( + KeysOrWeightedKeys keysOrWeightedKeys, Aggregate aggregate); + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The resulting sorted set from the union. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * Map payload1 = client.zunionWithScores(keyArray, Aggregate.MAX).get();
+     * assert Arrays.equals(payload1, Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0, gs("elem3"), 3.0));
+     *
+     * WeightedKeysBinary weightedKeys = new WeightedKeysBinary(List.of(Pair.of(gs("mySortedSet1"), 2.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * Map payload2 = client.zunionWithScores(weightedKeys, Aggregate.SUM).get();
+     * assert Arrays.equals(payload2, Map.of(gs("elem1"), 2.0, gs("elem2"), 4.0, gs("elem3"), 6.0));
+     * }
+ */ + CompletableFuture> zunionWithScores( + KeysOrWeightedKeysBinary keysOrWeightedKeys, Aggregate aggregate); + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys.
+ * To perform a zunion operation while specifying aggregation settings, use {@link + * #zunionWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The resulting sorted set from the union. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * Map payload1 = client.zunionWithScores(keyArray).get();
+     * assert Arrays.equals(payload1, Map.of("elem1", 1.0, "elem2", 2.0, "elem3", 3.0));
+     *
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 2.0), Pair.of("mySortedSet2", 2.0)));
+     * Map payload2 = client.zunionWithScores(keyArray).get();
+     * assert Arrays.equals(payload2, Map.of("elem1", 2.0, "elem2", 4.0, "elem3", 6.0));
+     * }
+ */ + CompletableFuture> zunionWithScores(KeysOrWeightedKeys keysOrWeightedKeys); + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys.
+ * To perform a zunion operation while specifying aggregation settings, use {@link + * #zunionWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The resulting sorted set from the union. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * Map payload1 = client.zunionWithScores(keyArray).get();
+     * assert Arrays.equals(payload1, Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0, gs("elem3"), 3.0));
+     *
+     * WeightedKeysBinary weightedKeys = new WeightedKeysBinary(List.of(Pair.of(gs("mySortedSet1"), 2.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * Map payload2 = client.zunionWithScores(keyArray).get();
+     * assert Arrays.equals(payload2, Map.of(gs("elem1"), 2.0, gs("elem2"), 4.0, gs("elem3"), 6.0));
+     * }
+ */ + CompletableFuture> zunionWithScores( + KeysOrWeightedKeysBinary keysOrWeightedKeys); + + /** + * Returns the intersection of members from sorted sets specified by the given keys. + *
+ * To get the elements with their scores, see {@link #zinterWithScores}. + * + * @apiNote When in cluster mode, all keys in keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return The resulting sorted set from the intersection. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * String[] payload = client.zinter(keyArray).get()
+     * assert payload.equals(new String[] {"elem1", "elem2", "elem3"});
+     *
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 2.0), Pair.of("mySortedSet2", 2.0)));
+     * String[] payload = client.zinter(weightedKeys).get()
+     * assert payload.equals(new String[] {"elem1", "elem2", "elem3"});
+     * }
+ */ + CompletableFuture zinter(KeyArray keys); + + /** + * Returns the intersection of members from sorted sets specified by the given keys. + *
+ * To get the elements with their scores, see {@link #zinterWithScores}. + * + * @apiNote When in cluster mode, all keys in keys must map to the same hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return The resulting sorted set from the intersection. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * GlideString[] payload = client.zinter(keyArray).get()
+     * assert payload.equals(new GlideString[] {gs("elem1"), gs("elem2"), gs("elem3")});
+     *
+     * WeightedKeysBinary weightedKeys = new WeightedKeysBinary(List.of(Pair.of(gs("mySortedSet1"), 2.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * GlideString[] payload = client.zinter(weightedKeys).get()
+     * assert payload.equals(new GlideString[] {gs("elem1"), gs("elem2"), gs("elem3")});
+     * }
+ */ + CompletableFuture zinter(KeyArrayBinary keys); + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. To perform a zinter operation while specifying + * aggregation settings, use {@link #zinterWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The resulting sorted set from the intersection. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * Map payload1 = client.zinterWithScores(keyArray).get();
+     * assert payload1.equals(Map.of("elem1", 1.0, "elem2", 2.0, "elem3", 3.0));
+     *
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 2.0), Pair.of("mySortedSet2", 2.0)));
+     * Map payload2 = client.zinterWithScores(weightedKeys).get();
+     * assert payload2.equals(Map.of("elem1", 2.0, "elem2", 4.0, "elem3", 6.0));
+     * }
+ */ + CompletableFuture> zinterWithScores(KeysOrWeightedKeys keysOrWeightedKeys); + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. To perform a zinter operation while specifying + * aggregation settings, use {@link #zinterWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return The resulting sorted set from the intersection. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * Map payload1 = client.zinterWithScores(keyArray).get();
+     * assert payload1.equals(Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0, gs("elem3"), 3.0));
+     *
+     * WeightedKeysBinary weightedKeys = new WeightedKeys(List.of(Pair.of(gs("mySortedSet1"), 2.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * Map payload2 = client.zinterWithScores(weightedKeys).get();
+     * assert payload2.equals(Map.of(gs("elem1"), 2.0, gs("elem2"), 4.0, gs("elem3"), 6.0));
+     * }
+ */ + CompletableFuture> zinterWithScores( + KeysOrWeightedKeysBinary keysOrWeightedKeys); + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The resulting sorted set from the intersection. + * @example + *
{@code
+     * KeyArray keyArray = new KeyArray(new String[] {"mySortedSet1", "mySortedSet2"});
+     * Map payload1 = client.zinterWithScores(keyArray, Aggregate.MAX).get();
+     * assert payload1.equals(Map.of("elem1", 1.0, "elem2", 2.0, "elem3", 3.0));
+     *
+     * WeightedKeys weightedKeys = new WeightedKeys(List.of(Pair.of("mySortedSet1", 2.0), Pair.of("mySortedSet2", 2.0)));
+     * Map payload2 = client.zinterWithScores(weightedKeys, Aggregate.SUM).get();
+     * assert payload2.equals(Map.of("elem1", 2.0, "elem2", 4.0, "elem3", 6.0));
+     * }
+ */ + CompletableFuture> zinterWithScores( + KeysOrWeightedKeys keysOrWeightedKeys, Aggregate aggregate); + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @apiNote When in cluster mode, all keys in keysOrWeightedKeys must map to the same + * hash slot. + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return The resulting sorted set from the intersection. + * @example + *
{@code
+     * KeyArrayBinary keyArray = new KeyArrayBinary(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")});
+     * Map payload1 = client.zinterWithScores(keyArray, AggregateBinary.MAX).get();
+     * assert payload1.equals(Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0, gs("elem3"), 3.0));
+     *
+     * WeightedKeysBinary weightedKeys = new WeightedKeysBinary(List.of(Pair.of(gs("mySortedSet1"), 2.0), Pair.of(gs("mySortedSet2"), 2.0)));
+     * Map payload2 = client.zinterWithScores(weightedKeys, AggregateBinary.SUM).get();
+     * assert payload2.equals(Map.of(gs("elem1"), 2.0, gs("elem2"), 4.0, gs("elem3"), 6.0));
+     * }
+ */ + CompletableFuture> zinterWithScores( + KeysOrWeightedKeysBinary keysOrWeightedKeys, Aggregate aggregate); + + /** + * Returns a random element from the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @return A String representing a random element from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be null. + * @example + *
{@code
+     * String payload1 = client.zrandmember("mySortedSet").get();
+     * assert payload1.equals("GLIDE");
+     *
+     * String payload2 = client.zrandmember("nonExistingSortedSet").get();
+     * assert payload2 == null;
+     * }
+ */ + CompletableFuture zrandmember(String key); + + /** + * Returns a random element from the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @return A String representing a random element from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be null. + * @example + *
{@code
+     * GlideString payload1 = client.zrandmember(gs("mySortedSet")).get();
+     * assert payload1.equals(gs("GLIDE"));
+     *
+     * GlideString payload2 = client.zrandmember(gs("nonExistingSortedSet")).get();
+     * assert payload2 == null;
+     * }
+ */ + CompletableFuture zrandmember(GlideString key); + + /** + * Returns a random element from the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return An array of elements from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + * @example + *
{@code
+     * String[] payload1 = client.zrandmember("mySortedSet", -3).get();
+     * assert payload1.equals(new String[] {"GLIDE", "GLIDE", "JAVA"});
+     *
+     * String[] payload2 = client.zrandmember("nonExistingSortedSet", 3).get();
+     * assert payload2.length == 0;
+     * }
+ */ + CompletableFuture zrandmemberWithCount(String key, long count); + + /** + * Retrieves random elements from the sorted set stored at key. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return An array of elements from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + * @example + *
{@code
+     * GlideString[] payload1 = client.zrandmemberWithCount(gs("mySortedSet"), -3).get();
+     * assert payload1.equals(new GlideString[] {gs("GLIDE"), gs("GLIDE"), gs("JAVA")});
+     *
+     * GlideString[] payload2 = client.zrandmemberWithCount(gs("nonExistingSortedSet"), 3).get();
+     * assert payload2.length == 0;
+     * }
+ */ + CompletableFuture zrandmemberWithCount(GlideString key, long count); + + /** + * Retrieves random elements along with their scores from the sorted set stored at key + * . + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows duplicates.
+ * @return An array of [element, score] arrays, where + * element is a String and score is a Double.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + * @example + *
{@code
+     * Object[][] data = client.zrandmemberWithCountWithScores("mySortedSet", -3).get();
+     * assert data.length == 3;
+     * for (Object[] memberScorePair : data) {
+     *     System.out.printf("Member: '%s', score: %d", memberScorePair[0], memberScorePair[1]);
+     * }
+     * }
+ */ + CompletableFuture zrandmemberWithCountWithScores(String key, long count); + + /** + * Retrieves random elements along with their scores from the sorted set stored at key + * . + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows duplicates.
+ * @return An array of [element, score] arrays, where + * element is a String and score is a Double.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + * @example + *
{@code
+     * Object[][] data = client.zrandmemberWithCountWithScores(gs("mySortedSet"), -3).get();
+     * assert data.length == 3;
+     * for (Object[] memberScorePair : data) {
+     *     System.out.printf("Member: '%s', score: %d", memberScorePair[0], memberScorePair[1]);
+     * }
+     * }
+ */ + CompletableFuture zrandmemberWithCountWithScores(GlideString key, long count); + + /** + * Increments the score of member in the sorted set stored at key by + * increment.
+ * If member does not exist in the sorted set, it is added with increment + * as its score. If key does not exist, a new sorted set with the specified + * member as its sole member is created. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param increment The score increment. + * @param member A member of the sorted set. + * @return The new score of member. + * @example + *
{@code
+     * Double score = client.zincrby("mySortedSet", -3.14, "value").get();
+     * assert score > 0; // member "value" existed in the set before score was altered
+     * }
+ */ + CompletableFuture zincrby(String key, double increment, String member); + + /** + * Increments the score of member in the sorted set stored at key by + * increment.
+ * If member does not exist in the sorted set, it is added with increment + * as its score. If key does not exist, a new sorted set with the specified + * member as its sole member is created. + * + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param increment The score increment. + * @param member A member of the sorted set. + * @return The new score of member. + * @example + *
{@code
+     * Double score = client.zincrby(gs("mySortedSet"), -3.14, gs("value")).get();
+     * assert score > 0; // member "value" existed in the set before score was altered
+     * }
+ */ + CompletableFuture zincrby(GlideString key, double increment, GlideString member); + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @return The cardinality of the intersection of the given sorted sets. + * @example + *
{@code
+     * Long length = client.zintercard(new String[] {"mySortedSet1", "mySortedSet2"}).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(String[] keys); + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @return The cardinality of the intersection of the given sorted sets. + * @example + *
{@code
+     * Long length = client.zintercard(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")}).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(GlideString[] keys); + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * If the intersection cardinality reaches limit partway through the computation, the + * algorithm will exit early and yield limit as the cardinality. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @param limit Specifies a maximum number for the intersection cardinality. If limit is set to + * 0 the range will be unlimited. + * @return The cardinality of the intersection of the given sorted sets, or the limit + * if reached. + * @example + *
{@code
+     * Long length = client.zintercard(new String[] {"mySortedSet1", "mySortedSet2"}, 5).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(String[] keys, long limit); + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * If the intersection cardinality reaches limit partway through the computation, the + * algorithm will exit early and yield limit as the cardinality. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @param limit Specifies a maximum number for the intersection cardinality. If limit is set to + * 0 the range will be unlimited. + * @return The cardinality of the intersection of the given sorted sets, or the limit + * if reached. + * @example + *
{@code
+     * Long length = client.zintercard(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")}, 5).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(GlideString[] keys, long limit); + + /** + * Iterates incrementally over a sorted set. + * + * @see valkey.io for details. + * @param key The key of the sorted set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the sorted set. The second element is always an + * + * Array of the subset of the sorted set held in key. The array in the + * second element is always a flattened series of String pairs, where the value + * is at even indices and the score is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *   result = client.zscan(key1, cursor).get();
+     *   cursor = result[0].toString();
+     *   Object[] stringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nZSCAN iteration:");
+     *   for (int i = 0; i < stringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", stringResults[i], stringResults[i + 1]);
+     *     if (i + 2 < stringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture zscan(String key, String cursor); + + /** + * Iterates incrementally over a sorted set. + * + * @see valkey.io for details. + * @param key The key of the sorted set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the sorted set. The second element is always an + * + * Array of the subset of the sorted set held in key. The array in the + * second element is always a flattened series of String pairs, where the value + * is at even indices and the score is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *   result = client.zscan(key1, cursor).get();
+     *   cursor = gs(result[0].toString());
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nZSCAN iteration:");
+     *   for (int i = 0; i < glideStringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", glideStringResults[i], glideStringResults[i + 1]);
+     *     if (i + 2 < glideStringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture zscan(GlideString key, GlideString cursor); + + /** + * Iterates incrementally over a sorted set. + * + * @see valkey.io for details. + * @param key The key of the sorted set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param zScanOptions The {@link ZScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the sorted set. The second element is always an + * + * Array of the subset of the sorted set held in key. The array in the + * second element is always a flattened series of String pairs, where the value + * is at even indices and the score is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * String cursor = "0";
+     * Object[] result;
+     * do {
+     *   result = client.zscan(key1, cursor, ZScanOptions.builder().matchPattern("*").count(20L).build()).get();
+     *   cursor = result[0].toString();
+     *   Object[] stringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nZSCAN iteration:");
+     *   for (int i = 0; i < stringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", stringResults[i], stringResults[i + 1]);
+     *     if (i + 2 < stringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals("0"));
+     * }
+ */ + CompletableFuture zscan(String key, String cursor, ZScanOptions zScanOptions); + + /** + * Iterates incrementally over a sorted set. + * + * @see valkey.io for details. + * @param key The key of the sorted set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param zScanOptions The {@link ZScanOptions}. + * @return An Array of Objects. The first element is always the + * cursor for the next iteration of results. "0" will be the cursor + * returned on the last iteration of the sorted set. The second element is always an + * + * Array of the subset of the sorted set held in key. The array in the + * second element is always a flattened series of String pairs, where the value + * is at even indices and the score is at odd indices. + * @example + *
{@code
+     * // Assume key contains a set with 200 member-score pairs
+     * GlideString cursor = gs("0");
+     * Object[] result;
+     * do {
+     *   result = client.zscan(key1, cursor, ZScanOptionsBinary.builder().matchPattern(gs("*")).count(20L).build()).get();
+     *   cursor = gs(result[0].toString());
+     *   Object[] glideStringResults = (Object[]) result[1];
+     *
+     *   System.out.println("\nZSCAN iteration:");
+     *   for (int i = 0; i < glideStringResults.length; i += 2) {
+     *     System.out.printf("{%s=%s}", glideStringResults[i], glideStringResults[i + 1]);
+     *     if (i + 2 < glideStringResults.length) {
+     *       System.out.print(", ");
+     *     }
+     *   }
+     * } while (!cursor.equals(gs("0")));
+     * }
+ */ + CompletableFuture zscan( + GlideString key, GlideString cursor, ZScanOptionsBinary zScanOptions); } diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index 2fa1ff4daf..c88bda1201 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -1,8 +1,21 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; -import glide.api.models.commands.StreamAddOptions; -import glide.api.models.commands.StreamAddOptions.StreamAddOptionsBuilder; +import glide.api.models.GlideString; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; +import glide.api.models.commands.stream.StreamAddOptionsBinary; +import glide.api.models.commands.stream.StreamAddOptionsBinary.StreamAddOptionsBinaryBuilder; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamPendingOptions; +import glide.api.models.commands.stream.StreamPendingOptionsBinary; +import glide.api.models.commands.stream.StreamRange; +import glide.api.models.commands.stream.StreamRange.IdBound; +import glide.api.models.commands.stream.StreamRange.InfRangeBound; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -10,7 +23,7 @@ * Supports commands and transactions for the "Stream Commands" group for standalone and cluster * clients. * - * @see Stream Commands + * @see Stream Commands */ public interface StreamBaseCommands { @@ -18,7 +31,7 @@ public interface StreamBaseCommands { * Adds an entry to the specified stream stored at key.
* If the key doesn't exist, the stream is created. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the stream. * @param values Field-value pairs to be added to the entry. * @return The id of the added entry. @@ -34,10 +47,26 @@ public interface StreamBaseCommands { * Adds an entry to the specified stream stored at key.
* If the key doesn't exist, the stream is created. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the stream. * @param values Field-value pairs to be added to the entry. - * @param options Stream add options. + * @return The id of the added entry. + * @example + *
{@code
+     * String streamId = client.xadd(gs("key"), Map.of(gs("name"), gs("Sara"), gs("surname"), gs("OConnor")).get();
+     * System.out.println("Stream: " + streamId);
+     * }
+ */ + CompletableFuture xadd(GlideString key, Map values); + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. * @return The id of the added entry, or null if {@link * StreamAddOptionsBuilder#makeStream(Boolean)} is set to false and no stream * with the matching key exists. @@ -52,4 +81,2117 @@ public interface StreamBaseCommands { * }
*/ CompletableFuture xadd(String key, Map values, StreamAddOptions options); + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. + * @return The id of the added entry, or null if {@link + * StreamAddOptionsBinaryBuilder#makeStream(Boolean)} is set to false and no + * stream with the matching key exists. + * @example + *
{@code
+     * // Option to use the existing stream, or return null if the stream doesn't already exist at "key"
+     * StreamAddOptionsBinary options = StreamAddOptions.builder().id(gs("sid")).makeStream(Boolean.FALSE).build();
+     * String streamId = client.xadd(gs("key"), Map.of(gs("name"), gs("Sara"), gs("surname"), gs("OConnor")), options).get();
+     * if (streamId != null) {
+     *     assert streamId.equals("sid");
+     * }
+     * }
+ */ + CompletableFuture xadd( + GlideString key, Map values, StreamAddOptionsBinary options); + + /** + * Reads entries from the given streams. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * Map xreadKeys = Map.of("streamKey", "0-0");
+     * Map> streamReadResponse = client.xread(xreadKeys).get();
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+ */ + CompletableFuture>> xread(Map keysAndIds); + + /** + * Reads entries from the given streams. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * Map xreadKeys = Map.of(gs("streamKey"), gs("0-0"));
+     * Map> streamReadResponse = client.xread(xreadKeys).get();
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+ */ + CompletableFuture>> xreadBinary( + Map keysAndIds); + + /** + * Reads entries from the given streams. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. + * @param options Options detailing how to read the stream {@link StreamReadOptions}. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // retrieve streamKey entries and block for 1 second if is no stream data
+     * Map xreadKeys = Map.of("streamKey", "0-0");
+     * StreamReadOptions options = StreamReadOptions.builder().block(1L).build();
+     * Map> streamReadResponse = client.xread(xreadKeys, options).get();
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+ */ + CompletableFuture>> xread( + Map keysAndIds, StreamReadOptions options); + + /** + * Reads entries from the given streams. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. + * @param options Options detailing how to read the stream {@link StreamReadOptions}. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // retrieve streamKey entries and block for 1 second if is no stream data
+     * Map xreadKeys = Map.of(gs("streamKey"), gs("0-0"));
+     * StreamReadOptions options = StreamReadOptions.builder().block(1L).build();
+     * Map> streamReadResponse = client.xread(xreadKeys, options).get();
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+ */ + CompletableFuture>> xreadBinary( + Map keysAndIds, StreamReadOptions options); + + /** + * Trims the stream by evicting older entries. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param options Stream trim options {@link StreamTrimOptions}. + * @return The number of entries deleted from the stream. + * @example + *
{@code
+     * // A nearly exact trimming of the stream to at least a length of 10
+     * Long trimmed = client.xtrim("key", new MaxLen(false, 10L)).get();
+     * System.out.println("Number of trimmed entries from stream: " + trimmed);
+     *
+     * // An exact trimming of the stream by minimum id of "0-3", limit of 10 entries
+     * Long trimmed = client.xtrim("key", new MinId(true, "0-3", 10L)).get();
+     * System.out.println("Number of trimmed entries from stream: " + trimmed);
+     * }
+ */ + CompletableFuture xtrim(String key, StreamTrimOptions options); + + /** + * Trims the stream by evicting older entries. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param options Stream trim options {@link StreamTrimOptions}. + * @return The number of entries deleted from the stream. + * @example + *
{@code
+     * // A nearly exact trimming of the stream to at least a length of 10
+     * Long trimmed = client.xtrim(gs("key"), new MaxLen(false, 10L)).get();
+     * System.out.println("Number of trimmed entries from stream: " + trimmed);
+     *
+     * // An exact trimming of the stream by minimum id of "0-3", limit of 10 entries
+     * Long trimmed = client.xtrim(gs("key"), new MinId(true, "0-3", 10L)).get();
+     * System.out.println("Number of trimmed entries from stream: " + trimmed);
+     * }
+ */ + CompletableFuture xtrim(GlideString key, StreamTrimOptions options); + + /** + * Returns the number of entries in the stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return The number of entries in the stream. If key does not exist, return 0 + * . + * @example + *
{@code
+     * Long num = client.xlen("key").get();
+     * assert num == 2L; // Stream has 2 entries
+     * }
+ */ + CompletableFuture xlen(String key); + + /** + * Returns the number of entries in the stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return The number of entries in the stream. If key does not exist, return 0 + * . + * @example + *
{@code
+     * Long num = client.xlen(gs("key")).get();
+     * assert num == 2L; // Stream has 2 entries
+     * }
+ */ + CompletableFuture xlen(GlideString key); + + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param ids An array of entry ids. + * @return The number of entries removed from the stream. This number may be less than the number + * of entries in ids, if the specified ids don't exist in the + * stream. + * @example + *
{@code
+     * Long num = client.xdel("key", new String[] {"1538561698944-0", "1538561698944-1"}).get();
+     * assert num == 2L; // Stream marked 2 entries as deleted
+     * }
+ */ + CompletableFuture xdel(String key, String[] ids); + + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param ids An array of entry ids. + * @return The number of entries removed from the stream. This number may be less than the number + * of entries in ids, if the specified ids don't exist in the + * stream. + * @example + *
{@code
+     * Long num = client.xdel("key", new GlideString[] {gs("1538561698944-0"), gs("1538561698944-1")}).get();
+     * assert num == 2L; // Stream marked 2 entries as deleted
+     * }
+ */ + CompletableFuture xdel(GlideString key, GlideString[] ids); + + /** + * Returns stream entries matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve all stream entries
+     * Map result = client.xrange("key", InfRangeBound.MIN, InfRangeBound.MAX).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * // Retrieve exactly one stream entry by id
+     * Map result = client.xrange("key", IdBound.of(streamId), IdBound.of(streamId)).get();
+     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
+     * }
+ */ + CompletableFuture> xrange(String key, StreamRange start, StreamRange end); + + /** + * Returns stream entries matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve all stream entries
+     * Map result = client.xrange(gs("key"), InfRangeBound.MIN, InfRangeBound.MAX).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * // Retrieve exactly one stream entry by id
+     * Map result = client.xrange(gs("key"), IdBound.of(streamId), IdBound.of(streamId)).get();
+     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
+     * }
+ */ + CompletableFuture> xrange( + GlideString key, StreamRange start, StreamRange end); + + /** + * Returns stream entries matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param count Maximum count of stream entries to return. + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve the first 2 stream entries
+     * Map result = client.xrange("key", InfRangeBound.MIN, InfRangeBound.MAX, 2).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * }
+ */ + CompletableFuture> xrange( + String key, StreamRange start, StreamRange end, long count); + + /** + * Returns stream entries matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param count Maximum count of stream entries to return. + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve the first 2 stream entries
+     * Map result = client.xrange(gs("key"), InfRangeBound.MIN, InfRangeBound.MAX, 2).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * }
+ */ + CompletableFuture> xrange( + GlideString key, StreamRange start, StreamRange end, long count); + + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(String, StreamRange, StreamRange)} but returns the entries in + * reverse order. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve all stream entries
+     * Map result = client.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * // Retrieve exactly one stream entry by id
+     * Map result = client.xrevrange("key", IdBound.of(streamId), IdBound.of(streamId)).get();
+     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
+     * }
+ */ + CompletableFuture> xrevrange( + String key, StreamRange end, StreamRange start); + + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(GlideString, StreamRange, StreamRange)} but returns the entries in + * reverse order. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve all stream entries
+     * Map result = client.xrevrange(gs("key"), InfRangeBound.MAX, InfRangeBound.MIN).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * // Retrieve exactly one stream entry by id
+     * Map result = client.xrevrange(gs("key"), IdBound.of(streamId), IdBound.of(streamId)).get();
+     * System.out.println("Stream ID: " + streamid + " -> " + Arrays.toString(result.get(streamid)));
+     * }
+ */ + CompletableFuture> xrevrange( + GlideString key, StreamRange end, StreamRange start); + + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(String, StreamRange, StreamRange, long)} but returns the entries + * in reverse order. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param count Maximum count of stream entries to return. + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve the first 2 stream entries
+     * Map result = client.xrange("key", InfRangeBound.MAX, InfRangeBound.MIN, 2).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * }
+ */ + CompletableFuture> xrevrange( + String key, StreamRange end, StreamRange start, long count); + + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(GlideString, StreamRange, StreamRange, long)} but returns the entries + * in reverse order. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param count Maximum count of stream entries to return. + * @return A Map of key to stream entry data, where entry data is an array of pairings with format [[field, entry], [field, entry], ...]. + * @example + *
{@code
+     * // Retrieve the first 2 stream entries
+     * Map result = client.xrange(gs("key"), InfRangeBound.MAX, InfRangeBound.MIN, 2).get();
+     * result.forEach((k, v) -> {
+     *     System.out.println("Stream ID: " + k);
+     *     for (int i = 0; i < v.length; i++) {
+     *         System.out.println(v[i][0] + ": " + v[i][1]);
+     *     }
+     * });
+     * }
+ */ + CompletableFuture> xrevrange( + GlideString key, StreamRange end, StreamRange start, long count); + + /** + * Creates a new consumer group uniquely identified by groupname for the stream + * stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupname The newly created consumer group name. + * @param id Stream entry ID that specifies the last delivered entry in the stream from the new + * group's perspective. The special ID "$" can be used to specify the last entry + * in the stream. + * @return OK. + * @example + *
{@code
+     * // Create the consumer group "mygroup", using zero as the starting ID:
+     * assert client.xgroupCreate("mystream", "mygroup", "0-0").get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupCreate(String key, String groupname, String id); + + /** + * Creates a new consumer group uniquely identified by groupname for the stream + * stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupname The newly created consumer group name. + * @param id Stream entry ID that specifies the last delivered entry in the stream from the new + * group’s perspective. The special ID "$" can be used to specify the last entry + * in the stream. + * @return OK. + * @example + *
{@code
+     * // Create the consumer group gs("mygroup"), using zero as the starting ID:
+     * assert client.xgroupCreate(gs("mystream"), gs("mygroup"), gs("0-0")).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupCreate(GlideString key, GlideString groupname, GlideString id); + + /** + * Creates a new consumer group uniquely identified by groupname for the stream + * stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The newly created consumer group name. + * @param id Stream entry ID that specifies the last delivered entry in the stream from the new + * group's perspective. The special ID "$" can be used to specify the last entry + * in the stream. + * @param options The group options {@link StreamGroupOptions}. + * @return OK. + * @example + *
{@code
+     * // Create the consumer group "mygroup", and the stream if it does not exist, after the last ID
+     * assert client.xgroupCreate("mystream", "mygroup", "$", new StreamGroupOptions(true)).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupCreate( + String key, String groupName, String id, StreamGroupOptions options); + + /** + * Creates a new consumer group uniquely identified by groupname for the stream + * stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The newly created consumer group name. + * @param id Stream entry ID that specifies the last delivered entry in the stream from the new + * group’s perspective. The special ID "$" can be used to specify the last entry + * in the stream. + * @param options The group options {@link StreamGroupOptions}. + * @return OK. + * @example + *
{@code
+     * // Create the consumer group gs("mygroup"), and the stream if it does not exist, after the last ID
+     * assert client.xgroupCreate(gs("mystream"), gs("mygroup"), gs("$"), new StreamGroupOptions(true)).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupCreate( + GlideString key, GlideString groupName, GlideString id, StreamGroupOptions options); + + /** + * Destroys the consumer group groupname for the stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupname The consumer group name to delete. + * @return true if the consumer group is destroyed. Otherwise, false. + * @example + *
{@code
+     * // Destroys the consumer group "mygroup"
+     * assert client.xgroupDestroy("mystream", "mygroup").get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupDestroy(String key, String groupname); + + /** + * Destroys the consumer group groupname for the stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupname The consumer group name to delete. + * @return true if the consumer group is destroyed. Otherwise, false. + * @example + *
{@code
+     * // Destroys the consumer group gs("mygroup")
+     * assert client.xgroupDestroy(gs("mystream"), gs("mygroup")).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupDestroy(GlideString key, GlideString groupname); + + /** + * Creates a consumer named consumer in the consumer group group for the + * stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The newly created consumer. + * @return true if the consumer is created. Otherwise, false. + * @example + *
{@code
+     * // Creates the consumer "myconsumer" in consumer group "mygroup"
+     * assert client.xgroupCreateConsumer("mystream", "mygroup", "myconsumer").get();
+     * }
+ */ + CompletableFuture xgroupCreateConsumer(String key, String group, String consumer); + + /** + * Creates a consumer named consumer in the consumer group group for the + * stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The newly created consumer. + * @return true if the consumer is created. Otherwise, false. + * @example + *
{@code
+     * // Creates the consumer gs("myconsumer") in consumer group gs("mygroup")
+     * assert client.xgroupCreateConsumer(gs("mystream"), gs("mygroup"), gs("myconsumer")).get();
+     * }
+ */ + CompletableFuture xgroupCreateConsumer( + GlideString key, GlideString group, GlideString consumer); + + /** + * Deletes a consumer named consumer in the consumer group group. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The consumer to delete. + * @return The number of pending messages the consumer had before it was deleted. + * @example + *
{@code
+     * // Deletes the consumer "myconsumer" in consumer group "mygroup"
+     * Long pendingMsgCount = client.xgroupDelConsumer("mystream", "mygroup", "myconsumer").get();
+     * System.out.println("Consumer 'myconsumer' had " +
+     *     + pendingMsgCount + " pending messages unclaimed.");
+     * }
+ */ + CompletableFuture xgroupDelConsumer(String key, String group, String consumer); + + /** + * Deletes a consumer named consumer in the consumer group group. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The consumer to delete. + * @return The number of pending messages the consumer had before it was deleted. + * @example + *
{@code
+     * // Deletes the consumer gs("myconsumer") in consumer group gs("mygroup")
+     * Long pendingMsgCount = client.xgroupDelConsumer(gs("mystream"), gs("mygroup"), gs("myconsumer")).get();
+     * System.out.println("Consumer 'myconsumer' had " +
+     *     + pendingMsgCount + " pending messages unclaimed.");
+     * }
+ */ + CompletableFuture xgroupDelConsumer( + GlideString key, GlideString group, GlideString consumer); + + /** + * Sets the last delivered ID for a consumer group. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @param id The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @return OK. + * @example + *
{@code
+     * // Update consumer group "mygroup", to set the last delivered entry ID.
+     * assert client.xgroupSetId("mystream", "mygroup", "0").get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupSetId(String key, String groupName, String id); + + /** + * Sets the last delivered ID for a consumer group. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @param id The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @return OK. + * @example + *
{@code
+     * // Update consumer group gs("mygroup"), to set the last delivered entry ID.
+     * assert client.xgroupSetId(gs("mystream"), gs("mygroup"), gs("0")).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupSetId(GlideString key, GlideString groupName, GlideString id); + + /** + * Sets the last delivered ID for a consumer group. + * + * @since Valkey 7.0 and above + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @param id The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @param entriesRead A value representing the number of stream entries already read by the group. + * @return OK. + * @example + *
{@code
+     * // Update consumer group "mygroup", to set the last delivered entry ID.
+     * assert client.xgroupSetId("mystream", "mygroup", "0", 1L).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupSetId(String key, String groupName, String id, long entriesRead); + + /** + * Sets the last delivered ID for a consumer group. + * + * @since Valkey 7.0 and above + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @param id The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @param entriesRead A value representing the number of stream entries already read by the group. + * @return OK. + * @example + *
{@code
+     * // Update consumer group gs("mygroup"), to set the last delivered entry ID.
+     * assert client.xgroupSetId(gs("mystream"), gs("mygroup"),gs("0"), 1L).get().equals("OK");
+     * }
+ */ + CompletableFuture xgroupSetId( + GlideString key, GlideString groupName, GlideString id, long entriesRead); + + /** + * Reads entries from the given streams owned by a consumer group. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. Use the special id of {@literal ">"} to receive only new messages. + * @param group The consumer group name. + * @param consumer The consumer name. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns null if there is no stream that can be served. + * @example + *
{@code
+     * // create a new stream at "mystream", with stream id "1-0"
+     * String streamId = client.xadd("mystream", Map.of("myfield", "mydata"), StreamAddOptions.builder().id("1-0").build()).get();
+     * assert client.xgroupCreate("mystream", "mygroup", "0-0").get().equals("OK"); // create the consumer group "mygroup"
+     * Map> streamReadResponse = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "myconsumer").get();
+     * // Returns "mystream": "1-0": {{"myfield", "mydata"}}
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+     * 
+ */ + CompletableFuture>> xreadgroup( + Map keysAndIds, String group, String consumer); + + /** + * Reads entries from the given streams owned by a consumer group. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. Use the special id of {@literal gs(">")} to receive only new messages. + * @param group The consumer group name. + * @param consumer The consumer name. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns null if there is no stream that can be served. + * @example + *
{@code
+     * // create a new stream at gs("mystream"), with stream id gs("1-0")
+     * String streamId = client.xadd(gs("mystream"), Map.of(gs("myfield"), gs("mydata")), StreamAddOptionsBinary.builder().id(gs("1-0")).build()).get();
+     * assert client.xgroupCreate(gs("mystream"), gs("mygroup"), gs("0-0")).get().equals("OK"); // create the consumer group gs("mygroup")
+     * Map> streamReadResponse = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("myconsumer")).get();
+     * // Returns gs("mystream"): gs("1-0"): {{gs("myfield"), gs("mydata")}}
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+     * 
+ */ + CompletableFuture>> xreadgroup( + Map keysAndIds, GlideString group, GlideString consumer); + + /** + * Reads entries from the given streams owned by a consumer group. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. Use the special id of {@literal ">"} to receive only new messages. + * @param group The consumer group name. + * @param consumer The consumer name. + * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns null if there is no stream that can be served. + * @example + *
{@code
+     * // create a new stream at "mystream", with stream id "1-0"
+     * String streamId = client.xadd("mystream", Map.of("myfield", "mydata"), StreamAddOptions.builder().id("1-0").build()).get();
+     * assert client.xgroupCreate("mystream", "mygroup", "0-0").get().equals("OK"); // create the consumer group "mygroup"
+     * StreamReadGroupOptions options = StreamReadGroupOptions.builder().count(1).build(); // retrieves only a single message at a time
+     * Map> streamReadResponse = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "myconsumer", options).get();
+     * // Returns "mystream": "1-0": {{"myfield", "mydata"}}
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+     * 
+ */ + CompletableFuture>> xreadgroup( + Map keysAndIds, + String group, + String consumer, + StreamReadGroupOptions options); + + /** + * Reads entries from the given streams owned by a consumer group. + * + * @apiNote When in cluster mode, all keys in keysAndIds must map to the same hash + * slot. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The + * Map is composed of a stream's key and the id of the entry after which the stream + * will be read. Use the special id of {@literal gs(">")} to receive only new messages. + * @param group The consumer group name. + * @param consumer The consumer name. + * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. + * @return A {@literal Map>} with stream + * keys, to Map of stream-ids, to an array of pairings with format [[field, entry], [field, entry], ...]. + * Returns null if there is no stream that can be served. + * @example + *
{@code
+     * // create a new stream at gs("mystream"), with stream id gs("1-0")
+     * String streamId = client.xadd(gs("mystream"), Map.of(gs("myfield"), gs("mydata")), StreamAddOptionsBinary.builder().id(gs("1-0")).build()).get();
+     * assert client.xgroupCreate(gs("mystream"), gs("mygroup"), gs("0-0")).get().equals("OK"); // create the consumer group gs("mygroup")
+     * StreamReadGroupOptions options = StreamReadGroupOptions.builder().count(1).build(); // retrieves only a single message at a time
+     * Map> streamReadResponse = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("myconsumer"), options).get();
+     * // Returns gs("mystream"): gs("1-0"): {{gs("myfield"), gs("mydata")}}
+     * for (var keyEntry : streamReadResponse.entrySet()) {
+     *     System.out.printf("Key: %s", keyEntry.getKey());
+     *     for (var streamEntry : keyEntry.getValue().entrySet()) {
+     *         Arrays.stream(streamEntry.getValue()).forEach(entity ->
+     *             System.out.printf("stream id: %s; field: %s; value: %s\n", streamEntry.getKey(), entity[0], entity[1])
+     *         );
+     *     }
+     * }
+     * 
+ */ + CompletableFuture>> xreadgroup( + Map keysAndIds, + GlideString group, + GlideString consumer, + StreamReadGroupOptions options); + + /** + * Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. + * This command should be called on a pending message so that such message does not get processed again. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param ids Stream entry ID to acknowledge and purge messages. + * @return The number of messages that were successfully acknowledged. + * @example + *
{@code
+     * String entryId = client.xadd("mystream", Map.of("myfield", "mydata")).get();
+     * // read messages from streamId
+     * var readResult = client.xreadgroup(Map.of("mystream", entryId), "mygroup", "my0consumer").get();
+     * // acknowledge messages on stream
+     * assert 1L == client.xack("mystream", "mygroup", new String[] {entryId}).get();
+     * 
+ */ + CompletableFuture xack(String key, String group, String[] ids); + + /** + * Returns the number of messages that were successfully acknowledged by the consumer group member of a stream. + * This command should be called on a pending message so that such message does not get processed again. + * + * @param key The key of the stream. + * @param group The consumer group name. + * @param ids Stream entry ID to acknowledge and purge messages. + * @return The number of messages that were successfully acknowledged. + * @example + *
{@code
+     * GlideString entryId = client.xadd(gs("mystream"), Map.of(gs("myfield"), gs("mydata")).get();
+     * // read messages from streamId
+     * var readResult = client.xreadgroup(Map.of(gs("mystream"), entryId), gs("mygroup"), gs("my0consumer")).get();
+     * // acknowledge messages on stream
+     * assert 1L == client.xack(gs("mystream"), gs("mygroup"), new GlideString[] {entryId}).get();
+     * 
+ */ + CompletableFuture xack(GlideString key, GlideString group, GlideString[] ids); + + /** + * Returns stream message summary information for pending messages matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @return An array that includes the summary of pending messages, with the format + * [NumOfMessages, StartId, EndId, [Consumer, NumOfMessages]], where: + *
    + *
  • NumOfMessages: The total number of pending messages for this consumer group. + *
  • StartId: The smallest ID among the pending messages. + *
  • EndId: The greatest ID among the pending messages. + *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every consumer + * in the consumer group with at least one pending message, and the number of pending messages it has. + *
+ * @example + *
{@code
+     * // Retrieve a summary of all pending messages from key "my_stream"
+     * Object[] result = client.xpending("my_stream", "my_group").get();
+     * System.out.println("Number of pending messages: " + result[0]);
+     * System.out.println("Start and End ID of messages: [" + result[1] + ", " + result[2] + "]");
+     * for (Object[] consumerResult : (Object[][]) result[3]) {
+     *     System.out.println("Number of Consumer messages: [" + consumerResult[0] + ", " + consumerResult[1] + "]");
+     * }
+ */ + CompletableFuture xpending(String key, String group); + + /** + * Returns stream message summary information for pending messages matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @return An array that includes the summary of pending messages, with the format + * [NumOfMessages, StartId, EndId, [Consumer, NumOfMessages]], where: + *
    + *
  • NumOfMessages: The total number of pending messages for this consumer group. + *
  • StartId: The smallest ID among the pending messages. + *
  • EndId: The greatest ID among the pending messages. + *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every consumer + * in the consumer group with at least one pending message, and the number of pending messages it has. + *
+ * @example + *
{@code
+     * // Retrieve a summary of all pending messages from key "my_stream"
+     * Object[] result = client.xpending(gs("my_stream"), gs("my_group")).get();
+     * System.out.println("Number of pending messages: " + result[0]);
+     * System.out.println("Start and End ID of messages: [" + result[1] + ", " + result[2] + "]");
+     * for (Object[] consumerResult : (Object[][]) result[3]) {
+     *     System.out.println("Number of Consumer messages: [" + consumerResult[0] + ", " + consumerResult[1] + "]");
+     * }
+ */ + CompletableFuture xpending(GlideString key, GlideString group); + + /** + * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * @param count Limits the number of messages returned. + * @return A 2D-array of 4-tuples containing extended message information with the format + * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: + *
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * @example + *
{@code
+     * // Retrieve up to 10 pending messages from key "my_stream" in extended form
+     * Object[][] result = client.xpending("my_stream", "my_group", InfRangeBound.MIN, InfRangeBound.MAX, 10L).get();
+     * for (Object[] messageResult : result) {
+     *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
+ */ + CompletableFuture xpending( + String key, String group, StreamRange start, StreamRange end, long count); + + /** + * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * @param count Limits the number of messages returned. + * @return A 2D-array of 4-tuples containing extended message information with the format + * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: + *
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * @example + *
{@code
+     * // Retrieve up to 10 pending messages from key "my_stream" in extended form
+     * Object[][] result = client.xpending(gs("my_stream"), gs("my_group"), InfRangeBound.MIN, InfRangeBound.MAX, 10L).get();
+     * for (Object[] messageResult : result) {
+     *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
+ */ + CompletableFuture xpending( + GlideString key, GlideString group, StreamRange start, StreamRange end, long count); + + /** + * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * @param count Limits the number of messages returned. + * @param options Stream add options {@link StreamPendingOptions}. + * @return A 2D-array of 4-tuples containing extended message information with the format + * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: + *
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * @example + *
{@code
+     * // Retrieve up to 10 pending messages from key "my_stream" and consumer "my_consumer" in extended form
+     * Object[][] result = client.xpending(
+     *     "my_stream",
+     *     "my_group",
+     *     InfRangeBound.MIN,
+     *     InfRangeBound.MAX,
+     *     10L,
+     *     StreamPendingOptions.builder().consumer("my_consumer").build()
+     * ).get();
+     * for (Object[] messageResult : result) {
+     *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
+ */ + CompletableFuture xpending( + String key, + String group, + StreamRange start, + StreamRange end, + long count, + StreamPendingOptions options); + + /** + * Returns an extended form of stream message information for pending messages matching a given range of IDs. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * @param count Limits the number of messages returned. + * @param options Stream add options {@link StreamPendingOptionsBinary}. + * @return A 2D-array of 4-tuples containing extended message information with the format + * [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ], where: + *
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ * @example + *
{@code
+     * // Retrieve up to 10 pending messages from key "my_stream" and consumer "my_consumer" in extended form
+     * Object[][] result = client.xpending(
+     *     gs("my_stream"),
+     *     gs("my_group"),
+     *     InfRangeBound.MIN,
+     *     InfRangeBound.MAX,
+     *     10L,
+     *     StreamPendingOptionsBinary.builder().consumer(gs("my_consumer")).build()
+     * ).get();
+     * for (Object[] messageResult : result) {
+     *     System.out.printf("Message %s from consumer %s was read %s times", messageResult[0], messageResult[1], messageResult[2]);
+     * }
+ */ + CompletableFuture xpending( + GlideString key, + GlideString group, + StreamRange start, + StreamRange end, + long count, + StreamPendingOptionsBinary options); + + /** + * Changes the ownership of a pending message. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids A array of entry ids. + * @return A Map of message entries with the format + * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + * @example + *
+     * // read messages from streamId for consumer1
+     * var readResult = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "consumer1").get();
+     * // "entryId" is now read, and we can assign the pending messages to consumer2
+     * Map results = client.xclaim("mystream", "mygroup", "consumer2", 0L, new String[] {entryId}).get();
+     * for (String key: results.keySet()) {
+     *     System.out.println(key);
+     *     for (String[] entry: results.get(key)) {
+     *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
+     *     }
+     * }
+     * 
+ */ + CompletableFuture> xclaim( + String key, String group, String consumer, long minIdleTime, String[] ids); + + /** + * Changes the ownership of a pending message. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids A array of entry ids. + * @return A Map of message entries with the format + * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + * @example + *
+     * // read messages from streamId for consumer1
+     * var readResult = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("consumer1")).get();
+     * // "entryId" is now read, and we can assign the pending messages to consumer2
+     * Map results = client.xclaim(gs("mystream"), gs("mygroup"), gs("consumer2"), 0L, new GlideString[] {entryId}).get();
+     * for (GlideString key: results.keySet()) {
+     *     System.out.println(key);
+     *     for (GlideString[] entry: results.get(key)) {
+     *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
+     *     }
+     * }
+     * 
+ */ + CompletableFuture> xclaim( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString[] ids); + + /** + * Changes the ownership of a pending message. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @param options Stream claim options {@link StreamClaimOptions}. + * @return A Map of message entries with the format + * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + * @example + *
+     * // assign (force) unread and unclaimed messages to consumer2
+     * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
+     * Map results = client.xclaim("mystream", "mygroup", "consumer2", 0L, new String[] {entryId}, options).get();
+     * for (String key: results.keySet()) {
+     *     System.out.println(key);
+     *     for (String[] entry: results.get(key)) {
+     *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
+     *     }
+     * }
+     * 
+ */ + CompletableFuture> xclaim( + String key, + String group, + String consumer, + long minIdleTime, + String[] ids, + StreamClaimOptions options); + + /** + * Changes the ownership of a pending message. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @param options Stream claim options {@link StreamClaimOptions}. + * @return A Map of message entries with the format + * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + * @example + *
+     * // assign (force) unread and unclaimed messages to consumer2
+     * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
+     * Map results = client.xclaim(gs("mystream"), gs("mygroup"), gs("consumer2"), 0L, new GlideString[] {entryId}, options).get();
+     * for (GlideString key: results.keySet()) {
+     *     System.out.println(key);
+     *     for (GlideString[] entry: results.get(key)) {
+     *         System.out.printf("{%s=%s}%n", entry[0], entry[1]);
+     *     }
+     * }
+     * 
+ */ + CompletableFuture> xclaim( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString[] ids, + StreamClaimOptions options); + + /** + * Changes the ownership of a pending message. This function returns an array with + * only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @return An array of message ids claimed by the consumer. + * @example + *
+     * // read messages from streamId for consumer1
+     * var readResult = client.xreadgroup(Map.of("mystream", ">"), "mygroup", "consumer1").get();
+     * // "entryId" is now read, and we can assign the pending messages to consumer2
+     * String[] results = client.xclaimJustId("mystream", "mygroup", "consumer2", 0L, new String[] {entryId}).get();
+     * for (String id: results) {
+     *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
+     * }
+     * 
+ */ + CompletableFuture xclaimJustId( + String key, String group, String consumer, long minIdleTime, String[] ids); + + /** + * Changes the ownership of a pending message. This function returns an array with + * only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @return An array of message ids claimed by the consumer. + * @example + *
+     * // read messages from streamId for consumer1
+     * var readResult = client.xreadgroup(Map.of(gs("mystream"), gs(">")), gs("mygroup"), gs("consumer1")).get();
+     * // "entryId" is now read, and we can assign the pending messages to consumer2
+     * GlideString[] results = client.xclaimJustId(gs("mystream"), gs("mygroup"), gs("consumer2"), 0L, new GlideString[] {entryId}).get();
+     * for (GlideString id: results) {
+     *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
+     * }
+     * 
+ */ + CompletableFuture xclaimJustId( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString[] ids); + + /** + * Changes the ownership of a pending message. This function returns an array with + * only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @param options Stream claim options {@link StreamClaimOptions}. + * @return An array of message ids claimed by the consumer. + * @example + *
+     * // assign (force) unread and unclaimed messages to consumer2
+     * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
+     * String[] results = client.xclaimJustId("mystream", "mygroup", "consumer2", 0L, new String[] {entryId}, options).get();
+     * for (String id: results) {
+     *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
+     * }
+     */
+    CompletableFuture xclaimJustId(
+            String key,
+            String group,
+            String consumer,
+            long minIdleTime,
+            String[] ids,
+            StreamClaimOptions options);
+
+    /**
+     * Changes the ownership of a pending message. This function returns an array with
+     * only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API.
+     *
+     * @see valkey.io for details.
+     * @param key The key of the stream.
+     * @param group The consumer group name.
+     * @param consumer The group consumer.
+     * @param minIdleTime The minimum idle time for the message to be claimed.
+     * @param ids An array of entry ids.
+     * @param options Stream claim options {@link StreamClaimOptions}.
+     * @return An array of message ids claimed by the consumer.
+     * @example
+     *     
+     * // assign (force) unread and unclaimed messages to consumer2
+     * StreamClaimOptions options = StreamClaimOptions.builder().force().build();
+     * GlideString[] results = client.xclaimJustId(gs("mystream"), gs("mygroup"), gs("consumer2"), 0L, new GlideString[] {entryId}, options).get();
+     * for (GlideString id: results) {
+     *     System.out.printf("consumer2 claimed stream entry ID: %s %n", id);
+     * }
+     */
+    CompletableFuture xclaimJustId(
+            GlideString key,
+            GlideString group,
+            GlideString consumer,
+            long minIdleTime,
+            GlideString[] ids,
+            StreamClaimOptions options);
+
+    /**
+     * Returns the list of all consumer groups and their attributes for the stream stored at key
+     * .
+     *
+     * @see valkey.io for details.
+     * @param key The key of the stream.
+     * @return An Array of Maps, where each mapping represents the
+     *     attributes of a consumer group for the stream at key.
+     * @example
+     *     
{@code
+     * Map[] groups = client.xinfoGroups("key").get();
+     * for (int i = 0; i < groups.length; i ++) {
+     *     System.out.println("Info of group: " + groups[0].get("name"));
+     *     System.out.println("\tname: " + groups[0].get("name"));
+     *     System.out.println("\tconsumers: " + groups[0].get("consumers"));
+     *     System.out.println("\tpending: " + groups[0].get("pending"));
+     *     System.out.println("\tlast-delivered-id: " + groups[0].get("last-delivered-id"));
+     *     System.out.println("\tentries-read: " + groups[0].get("entries-read"));
+     *     System.out.println("\tlag: " + groups[0].get("lag"));
+     * }
+     * }
+ */ + CompletableFuture[]> xinfoGroups(String key); + + /** + * Returns the list of all consumer groups and their attributes for the stream stored at key + * . + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return An Array of Maps, where each mapping represents the + * attributes of a consumer group for the stream at key. + * @example + *
{@code
+     * Map[] groups = client.xinfoGroups(gs("key")).get();
+     * for (int i = 0; i < groups.length; i ++) {
+     *     System.out.println("Info of group: " + groups[0].get(gs("name")));
+     *     System.out.println("\tname: " + groups[0].get(gs("name")));
+     *     System.out.println("\tconsumers: " + groups[0].get(gs("consumers")));
+     *     System.out.println("\tpending: " + groups[0].get(gs("pending")));
+     *     System.out.println("\tlast-delivered-id: " + groups[0].get(gs("last-delivered-id")));
+     *     System.out.println("\tentries-read: " + groups[0].get(gs("entries-read")));
+     *     System.out.println("\tlag: " + groups[0].get(gs("lag")));
+     * }
+     * }
+ */ + CompletableFuture[]> xinfoGroups(GlideString key); + + /** + * Returns the list of all consumers and their attributes for the given consumer group of the + * stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @return An Array of Maps, where each mapping contains the attributes + * of a consumer for the given consumer group of the stream at key. + * @example + *
{@code
+     * Map[] consumers = client.xinfoConsumers("key", "groupName").get();
+     * for (int i = 0; i < consumers.length; i ++) {
+     *     System.out.println("Info of consumer: " + consumers[0].get("name"));
+     *     System.out.println("\tname: " + consumers[0].get("name"));
+     *     System.out.println("\tpending: " + consumers[0].get("pending"));
+     *     System.out.println("\tidle: " + consumers[0].get("idle"));
+     *     System.out.println("\tinactive: " + consumers[0].get("inactive"));
+     * }
+     * }
+ */ + CompletableFuture[]> xinfoConsumers(String key, String groupName); + + /** + * Returns the list of all consumers and their attributes for the given consumer group of the + * stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @return An Array of Maps, where each mapping contains the attributes + * of a consumer for the given consumer group of the stream at key. + * @example + *
{@code
+     * Map[] consumers = client.xinfoConsumers(gs("key"), gs("groupName")).get();
+     * for (int i = 0; i < consumers.length; i ++) {
+     *     System.out.println("Info of consumer: " + consumers[0].get(gs("name")));
+     *     System.out.println("\tname: " + consumers[0].get(gs("name")));
+     *     System.out.println("\tpending: " + consumers[0].get(gs("pending")));
+     *     System.out.println("\tidle: " + consumers[0].get(gs("idle")));
+     *     System.out.println("\tinactive: " + consumers[0].get(gs("inactive")));
+     * }
+     * }
+ */ + CompletableFuture[]> xinfoConsumers( + GlideString key, GlideString groupName); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the + * values being a 2D list of the field-value pairs in the format + * [[field1, value1], [field2, value2], ...]. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaim("my_stream", "my_group", "my_consumer", 3_600_000L, "0-0").get();
+     * assertEquals(streamid_1, result[0]);
+     * assertDeepEquals(Map.of(streamid_0, new String[][] {{"f1", "v1"}}),result[1]);
+     * assertDeepEquals(new Object[] {},result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaim( + String key, String group, String consumer, long minIdleTime, String start); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the + * values being a 2D list of the field-value pairs in the format + * [[field1, value1], [field2, value2], ...]. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaim(gs("my_stream"), gs("my_group"), gs("my_consumer"), 3_600_000L, gs("0-0")).get();
+     * assertEquals(streamid_1, result[0]);
+     * assertDeepEquals(Map.of(streamid_0, new GlideString[][] {{gs("f1"), gs("v1")}}),result[1]);
+     * assertDeepEquals(new Object[] {},result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaim( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString start); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count Limits the number of claimed entries to the specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the + * values being a 2D list of the field-value pairs in the format + * [[field1, value1], [field2, value2], ...]. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaim("my_stream", "my_group", "my_consumer", 3_600_000L, "0-0", 1L).get();
+     * assertEquals(streamid_1, result[0]);
+     * assertDeepEquals(Map.of(streamid_0, new String[][] {{"f1", "v1"}}),result[1]);
+     * assertDeepEquals(new Object[] {},result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaim( + String key, String group, String consumer, long minIdleTime, String start, long count); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count Limits the number of claimed entries to the specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the + * values being a 2D list of the field-value pairs in the format + * [[field1, value1], [field2, value2], ...]. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaim(gs("my_stream"), gs("my_group"), gs("my_consumer"), 3_600_000L, gs("0-0"), 1L).get();
+     * assertEquals(streamid_1, result[0]);
+     * assertDeepEquals(Map.of(streamid_0, new GlideString[][] {{gs("f1"), gs("v1")}}),result[1]);
+     * assertDeepEquals(new Object[] {},result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaim( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString start, + long count); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. This command + * uses the JUSTID argument to further specify that the return value should contain a + * list of claimed IDs without their field-value info. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A list of the IDs for the claimed entries. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaimJustId("my_stream", "my_group", "my_consumer", 3_600_000L, "0-0").get();
+     * assertEquals(zeroStreamId, result[0]);
+     * assertDeepEquals(new String[] {streamid_0, streamid_1, streamid_3}, result[1]);
+     * assertDeepEquals(new Object[] {}, result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaimJustId( + String key, String group, String consumer, long minIdleTime, String start); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. This command + * uses the JUSTID argument to further specify that the return value should contain a + * list of claimed IDs without their field-value info. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A list of the IDs for the claimed entries. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaimJustId(gs("my_stream"), gs("my_group"), gs("my_consumer"), 3_600_000L, gs("0-0")).get();
+     * assertEquals(zeroStreamId, result[0]);
+     * assertDeepEquals(new GlideString[] {streamid_0, streamid_1, streamid_3}, result[1]);
+     * assertDeepEquals(new Object[] {}, result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaimJustId( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString start); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. This command + * uses the JUSTID argument to further specify that the return value should contain a + * list of claimed IDs without their field-value info. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count Limits the number of claimed entries to the specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A list of the IDs for the claimed entries. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaimJustId("my_stream", "my_group", "my_consumer", 3_600_000L, "0-0", 1L).get();
+     * assertEquals(zeroStreamId, result[0]);
+     * assertDeepEquals(new String[] {streamid_0, streamid_1, streamid_3}, result[1]);
+     * assertDeepEquals(new Object[] {}, result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaimJustId( + String key, String group, String consumer, long minIdleTime, String start, long count); + + /** + * Transfers ownership of pending stream entries that match the specified criteria. This command + * uses the JUSTID argument to further specify that the return value should contain a + * list of claimed IDs without their field-value info. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count Limits the number of claimed entries to the specified value. + * @return An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A list of the IDs for the claimed entries. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ * + * @example + *
{@code
+     * Object[] result = client.xautoclaimJustId(gs("my_stream"), gs("my_group"), gs("my_consumer"), 3_600_000L, gs("0-0"), 1L).get();
+     * assertEquals(zeroStreamId, result[0]);
+     * assertDeepEquals(new GlideString[] {streamid_0, streamid_1, streamid_3}, result[1]);
+     * assertDeepEquals(new Object[] {}, result[2]); // version 7.0.0 or above
+     * }
+ */ + CompletableFuture xautoclaimJustId( + GlideString key, + GlideString group, + GlideString consumer, + long minIdleTime, + GlideString start, + long count); + + /** + * Returns information about the stream stored at key key.
+ * To get more detailed information use {@link #xinfoStreamFull(String)} or {@link + * #xinfoStreamFull(String, int)}. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return A Map of stream information for the given key. See the + * example for a sample response. + * @example + *
{@code
+     * // example of using the API:
+     * Map response = client.xinfoStream("myStream").get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "length", 4L,
+     *     "radix-tree-keys", 1L,
+     *     "radix-tree-nodes", 2L,
+     *     "last-generated-id", "1719877599564-0",
+     *     "max-deleted-entry-id", "0-0",
+     *     "entries-added", 4L,
+     *     "recorded-first-entry-id", "1719710679916-0",
+     *     "groups", 1L,
+     *     "first-entry", new Object {
+     *         "1719710679916-0",
+     *         new String[] {
+     *             "foo", "bar",
+     *             "foo", "bar2",
+     *             "some_field", "some_value"
+     *         }},
+     *     "last-entry", new Object {
+     *         "1719877599564-0",
+     *         new String[] {
+     *             { "e4_f", "e4_v" }
+     *         }}
+     * );
+     * // Stream information for "my_stream". Note that "first-entry" and "last-entry" could both be `null` if stream is empty.
+     * }
+ */ + CompletableFuture> xinfoStream(String key); + + /** + * Returns information about the stream stored at key key.
+ * To get more detailed information use {@link #xinfoStreamFull(GlideString)} or {@link + * #xinfoStreamFull(GlideString, int)}. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return A Map of stream information for the given key. See the + * example for a sample response. + * @example + *
{@code
+     * // example of using the API:
+     * Map response = client.xinfoStream(gs("myStream")).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     gs("length"), 4L,
+     *     gs("radix-tree-keys"), 1L,
+     *     gs("radix-tree-nodes"), 2L,
+     *     gs("last-generated-id"), gs("1719877599564-0"),
+     *     gs("max-deleted-entry-id"), gs("0-0"),
+     *     gs("entries-added"), 4L,
+     *     gs("recorded-first-entry-id"), gs("1719710679916-0"),
+     *     gs("groups"), 1L,
+     *     gs("first-entry"), new Object {
+     *         gs("1719710679916-0"),
+     *         new GlideString[] {
+     *             gs("foo"), gs("bar"),
+     *             gs("foo"), gs("bar2"),
+     *             gs("some_field"), gs("some_value")
+     *         }},
+     *     gs("last-entry", Object {
+     *         gs("1719877599564-0"),
+     *         new GlideString[] {
+     *             { gs("e4_f"), gs("e4_v") }
+     *         }}
+     * );
+     * // Stream information for "my_stream". Note that "first-entry" and "last-entry" could both be `null` if stream is empty.
+     * }
+ */ + CompletableFuture> xinfoStream(GlideString key); + + /** + * Returns verbose information about the stream stored at key key.
+ * The output is limited by first 10 PEL entries. + * + * @since Valkey 6.0 and above. + * @see valkey.io for details. + * @param key The key of the stream. + * @return A Map of detailed stream information for the given key. See + * the example for a sample response. + * @example + *
{@code
+     * // example of using the API:
+     * Map response = client.xinfoStreamFull("myStream").get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "length", 4L,
+     *     "radix-tree-keys", 1L,
+     *     "radix-tree-nodes", 2L,
+     *     "last-generated-id", "1719877599564-0",
+     *     "max-deleted-entry-id", "0-0",
+     *     "entries-added", 4L,
+     *     "recorded-first-entry-id", "1719710679916-0",
+     *     "entries", new Object {
+     *         "1719710679916-0",
+     *         new String[] {
+     *             "foo", "bar",
+     *             "foo", "bar2",
+     *             "some_field", "some_value"
+     *         },
+     *         "1719710688676-0",
+     *         new String[] {
+     *             { "foo", "bar2" },
+     *         },
+     *     },
+     *     "groups", new Map[] {
+     *         Map.of(
+     *             "name", "mygroup",
+     *             "last-delivered-id", "1719710688676-0",
+     *             "entries-read", 2L,
+     *             "lag", 0L,
+     *             "pel-count", 2L,
+     *             "pending", new Object[][] { {
+     *                     "1719710679916-0",
+     *                     "Alice",
+     *                     1719710707260L,
+     *                     1L,
+     *                 }, {
+     *                     "1719710688676-0",
+     *                     "Alice",
+     *                     1719710718373L,
+     *                     1L
+     *                 } },
+     *             "consumers", new Map[] {
+     *                 Map.of(
+     *                     "name", "Alice",
+     *                     "seen-time", 1719710718373L,
+     *                     "active-time", 1719710718373L,
+     *                     "pel-count", 2L,
+     *                     "pending", new Object[][] { {
+     *                             "1719710679916-0",
+     *                             1719710707260L,
+     *                             1L,
+     *                         }, {
+     *                             "1719710688676-0",
+     *                             1719710718373L,
+     *                             1L
+     *                         } }
+     *                 )
+     *             })
+     * }); // Detailed stream information for "my_stream".
+     * }
+ */ + CompletableFuture> xinfoStreamFull(String key); + + /** + * Returns verbose information about the stream stored at key key.
+ * The output is limited by first 10 PEL entries. + * + * @since Valkey 6.0 and above. + * @see valkey.io for details. + * @param key The key of the stream. + * @return A Map of detailed stream information for the given key. See + * the example for a sample response. + * @example + *
{@code
+     * // example of using the API:
+     * Map response = client.xinfoStreamFull(gs("myStream")).get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     gs("length"), 4L,
+     *     gs("radix-tree-keys"), 1L,
+     *     gs("radix-tree-nodes"), 2L,
+     *     gs("last-generated-id"), gs("1719877599564-0"),
+     *     gs("max-deleted-entry-id"), gs("0-0"),
+     *     gs("entries-added"), 4L,
+     *     gs("recorded-first-entry-id"), gs("1719710679916-0"),
+     *     gs("entries"), new Object {
+     *         gs("1719710679916-0"),
+     *         new GlideString[] {
+     *             gs("foo"), gs("bar"),
+     *             gs("foo"), gs("bar2"),
+     *             gs("some_field"), gs("some_value")
+     *         },
+     *         gs("1719710688676-0"),
+     *         new GlideString[] {
+     *             { gs("foo"), gs("bar2") },
+     *         },
+     *     },
+     *     gs("groups"), new Map[] {
+     *         Map.of(
+     *             gs("name"), gs("mygroup"),
+     *             gs("last-delivered-id"), gs("1719710688676-0"),
+     *             gs("entries-read"), 2L,
+     *             gs("lag"), 0L,
+     *             gs("pel-count"), 2L,
+     *             gs("pending"), new Object[][] { {
+     *                     gs("1719710679916-0"),
+     *                     gs("Alice"),
+     *                     1719710707260L,
+     *                     1L,
+     *                 }, {
+     *                     gs("1719710688676-0"),
+     *                     gs("Alice"),
+     *                     1719710718373L,
+     *                     1L
+     *                 } },
+     *             gs("consumers"), new Map[] {
+     *                 Map.of(
+     *                     gs("name"), gs("Alice"),
+     *                     gs("seen-time"), 1719710718373L,
+     *                     gs("active-time"), 1719710718373L,
+     *                     gs("pel-count"), 2L,
+     *                     gs("pending"), new Object[][] { {
+     *                             gs("1719710679916-0"),
+     *                             1719710707260L,
+     *                             1L,
+     *                         }, {
+     *                             gs("1719710688676-0"),
+     *                             1719710718373L,
+     *                             1L
+     *                         } }
+     *                 )
+     *             })
+     * }); // Detailed stream information for "my_stream".
+     * }
+ */ + CompletableFuture> xinfoStreamFull(GlideString key); + + /** + * Returns verbose information about the stream stored at key key. + * + * @since Valkey 6.0 and above. + * @see valkey.io for details. + * @param key The key of the stream. + * @param count The number of stream and PEL entries that are returned. Value of 0 + * means that all entries will be returned. + * @return A Map of detailed stream information for the given key. + * @example + *
{@code
+     * // example of using the API:
+     * Map response = client.xinfoStreamFull("myStream", 42).get();
+     * }
+ * The response has the same format as {@link #xinfoStreamFull(String)}. + */ + CompletableFuture> xinfoStreamFull(String key, int count); + + /** + * Returns verbose information about the stream stored at key key. + * + * @since Valkey 6.0 and above. + * @see valkey.io for details. + * @param key The key of the stream. + * @param count The number of stream and PEL entries that are returned. Value of 0 + * means that all entries will be returned. + * @return A Map of detailed stream information for the given key. + * @example + *
{@code
+     * // example of using the API:
+     * Map response = client.xinfoStreamFull(gs("myStream"), 42).get();
+     * }
+ * The response has the same format as {@link #xinfoStreamFull(GlideString)}. + */ + CompletableFuture> xinfoStreamFull(GlideString key, int count); } diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 0f6919be12..38f6c50117 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -1,6 +1,8 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; +import glide.api.models.commands.GetExOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.ConditionalSet; import glide.api.models.commands.SetOptions.SetOptionsBuilder; @@ -11,18 +13,33 @@ * Supports commands and transactions for the "String Commands" group for standalone and cluster * clients. * - * @see String Commands + * @see String Commands */ public interface StringBaseCommands { + /** Valkey API keyword used to indicate that the length of the lcs should be returned. */ + public static final String LEN_VALKEY_API = "LEN"; + + /** IDX option string to include in the LCS command. */ + public static final String IDX_COMMAND_STRING = "IDX"; + + /** MINMATCHLEN option string to include in the LCS command. */ + public static final String MINMATCHLEN_COMMAND_STRING = "MINMATCHLEN"; + + /** WITHMATCHLEN option string to include in the LCS command. */ + public static final String WITHMATCHLEN_COMMAND_STRING = "WITHMATCHLEN"; + + /** Key for LCS matches result. */ + public static final String LCS_MATCHES_RESULT_KEY = "matches"; + /** * Gets the value associated with the given key, or null if no such * value exists. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to retrieve from the database. - * @return Response from Redis. If key exists, returns the value of - * key as a String. Otherwise, return null. + * @return If key exists, returns the value of key as a + * String. Otherwise, return null. * @example *
{@code
      * String value = client.get("key").get();
@@ -34,13 +51,138 @@ public interface StringBaseCommands {
      */
     CompletableFuture get(String key);
 
+    /**
+     * Gets the value associated with the given key, or null if no such
+     * value exists.
+     *
+     * @see valkey.io for details.
+     * @param key The key to retrieve from the database.
+     * @return If key exists, returns the value of key as a
+     *     String. Otherwise, return null.
+     * @example
+     *     
{@code
+     * GlideString value = client.get(gs("key")).get();
+     * assert Arrays.equals(value.getString(), "value");
+     *
+     * String value = client.get("non_existing_key").get();
+     * assert value.equals(null);
+     * }
+ */ + CompletableFuture get(GlideString key); + + /** + * Gets a string value associated with the given key and deletes the key. + * + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @return If key exists, returns the value of key. + * Otherwise, return null. + * @example + *
{@code
+     * String value = client.getdel("key").get();
+     * assert value.equals("value");
+     *
+     * String value = client.getdel("key").get();
+     * assert value.equals(null);
+     * }
+ */ + CompletableFuture getdel(String key); + + /** + * Gets a string value associated with the given key and deletes the key. + * + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @return If key exists, returns the value of key. + * Otherwise, return null. + * @example + *
{@code
+     * GlideString value = client.getdel(gs("key")).get();
+     * assert value.getString().equals("value");
+     *
+     * GlideString value = client.getdel(gs("key")).get();
+     * assert value.equals(null);
+     * }
+ */ + CompletableFuture getdel(GlideString key); + + /** + * Gets the value associated with the given key. + * + * @since Valkey 6.2.0. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @return If key exists, return the value of the key. + * Otherwise, return null. + * @example + *
{@code
+     * String value = client.getex("key").get();
+     * assert value.equals("value");
+     * }
+ */ + CompletableFuture getex(String key); + + /** + * Gets the value associated with the given key. + * + * @since Valkey 6.2.0. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @return If key exists, return the value of the key. + * Otherwise, return null. + * @example + *
{@code
+     * GlideString value = client.getex(gs("key")).get();
+     * assert value.equals(gs("value"));
+     * }
+ */ + CompletableFuture getex(GlideString key); + + /** + * Gets the value associated with the given key. + * + * @since Valkey 6.2.0. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @param options The {@link GetExOptions} options. + * @return If key exists, return the value of the key. + * Otherwise, return null. + * @example + *
{@code
+     * String response = client.set("key", "value").get();
+     * assert response.equals(OK);
+     * String value = client.getex("key", GetExOptions.Seconds(10L)).get();
+     * assert value.equals("value");
+     * }
+ */ + CompletableFuture getex(String key, GetExOptions options); + + /** + * Gets the value associated with the given key. + * + * @since Valkey 6.2.0. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @param options The {@link GetExOptions} options. + * @return If key exists, return the value of the key. + * Otherwise, return null. + * @example + *
{@code
+     * String response = client.set(gs("key"), gs("value").get();
+     * assert response.equals(OK);
+     * GlideString value = client.getex(gs("key"), GetExOptions.Seconds(10L)).get();
+     * assert value.equals(gs("value"));
+     * }
+ */ + CompletableFuture getex(GlideString key, GetExOptions options); + /** * Sets the given key with the given value. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to store. * @param value The value to store with the given key. - * @return Response from Redis containing "OK". + * @return A simple "OK" response. * @example *
{@code
      * String value = client.set("key", "value").get();
@@ -49,16 +191,30 @@ public interface StringBaseCommands {
      */
     CompletableFuture set(String key, String value);
 
+    /**
+     * Sets the given key with the given value.
+     *
+     * @see valkey.io for details.
+     * @param key The key to store.
+     * @param value The value to store with the given key.
+     * @return A simple "OK" response.
+     * @example
+     *     
{@code
+     * GlideString value = client.set(gs("key"), gs("value")).get();
+     * assert value.getString().equals("OK");
+     * }
+ */ + CompletableFuture set(GlideString key, GlideString value); + /** * Sets the given key with the given value. Return value is dependent on the passed options. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to store. * @param value The value to store with the given key. * @param options The Set options. - * @return Response from Redis containing a String or null response. If - * the value is successfully set, return "OK". If value isn't set because of - * {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} + * @return If the value is successfully set, return "OK". If value isn't set because + * of {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} * conditions, return null. If {@link SetOptionsBuilder#returnOldValue(boolean)} * is set, return the old value as a String. * @example @@ -70,10 +226,32 @@ public interface StringBaseCommands { */ CompletableFuture set(String key, String value, SetOptions options); + /** + * Sets the given key with the given value. Return value is dependent on the passed options. + * + * @see valkey.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @param options The Set options. + * @return If the value is successfully set, return "OK". If value isn't set because + * of {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} + * conditions, return null. If {@link SetOptionsBuilder#returnOldValue(boolean)} + * is set, return the old value as a String. + * @example + *
{@code
+     * SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).expiry(Seconds(5L)).build();
+     * String value = client.set("key".getBytes(), "value".getBytes(), options).get();
+     * assert value.equals("OK");
+     * }
+ */ + CompletableFuture set(GlideString key, GlideString value, SetOptions options); + /** * Retrieves the values of multiple keys. * - * @see redis.io for details. + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. * @param keys A list of keys to retrieve values for. * @return An array of values corresponding to the provided keys.
* If a keyis not found, its corresponding value in the list will be null @@ -86,10 +264,30 @@ public interface StringBaseCommands { */ CompletableFuture mget(String[] keys); + /** + * Retrieves the values of multiple keys. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys A list of keys to retrieve values for. + * @return An array of values corresponding to the provided keys.
+ * If a keyis not found, its corresponding value in the list will be null + * . + * @example + *
{@code
+     * GlideString[] values = client.mget(new GlideString[] {"key1", "key2"}).get();
+     * assert values.equals(new GlideString[] {"value1", "value2"});
+     * }
+ */ + CompletableFuture mget(GlideString[] keys); + /** * Sets multiple keys to multiple values in a single operation. * - * @see redis.io for details. + * @apiNote When in cluster mode, the command may route to multiple nodes when keys in + * keyValueMap map to different hash slots. + * @see valkey.io for details. * @param keyValueMap A key-value map consisting of keys and their respective values to set. * @return Always OK. * @example @@ -100,11 +298,61 @@ public interface StringBaseCommands { */ CompletableFuture mset(Map keyValueMap); + /** + * Sets multiple keys to multiple values in a single operation. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys in + * keyValueMap map to different hash slots. + * @see valkey.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return Always OK. + * @example + *
{@code
+     * String result = client.msetBinary(Map.of(gs("key1"), gs("value1"), gs("key2"), gs("value2")}).get();
+     * assert result.equals("OK"));
+     * }
+ */ + CompletableFuture msetBinary(Map keyValueMap); + + /** + * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + * more keys already exist, the entire operation fails. + * + * @apiNote When in cluster mode, all keys in keyValueMap must map to the same hash + * slot. + * @see valkey.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return true if all keys were set. false if no key was set. + * @example + *
{@code
+     * Boolean result = client.msetnx(Map.of("key1", "value1", "key2", "value2"}).get();
+     * assert result;
+     * }
+ */ + CompletableFuture msetnx(Map keyValueMap); + + /** + * Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + * more keys already exist, the entire operation fails. + * + * @apiNote When in cluster mode, all keys in keyValueMap must map to the same hash + * slot. + * @see valkey.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return true if all keys were set. false if no key was set. + * @example + *
{@code
+     * Boolean result = client.msetnxBinary(Map.of(gs("key1"), gs("value1"), gs("key2"), gs("value2")}).get();
+     * assert result;
+     * }
+ */ + CompletableFuture msetnxBinary(Map keyValueMap); + /** * Increments the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to increment its value. * @return The value of key after the increment. * @example @@ -115,11 +363,26 @@ public interface StringBaseCommands { */ CompletableFuture incr(String key); + /** + * Increments the number stored at key by one. If key does not exist, it + * is set to 0 before performing the operation. + * + * @see valkey.io for details. + * @param key The key to increment its value. + * @return The value of key after the increment. + * @example + *
{@code
+     * Long num = client.incr(gs("key")).get();
+     * assert num == 5L;
+     * }
+ */ + CompletableFuture incr(GlideString key); + /** * Increments the number stored at key by amount. If key * does not exist, it is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to increment its value. * @param amount The amount to increment. * @return The value of key after the increment. @@ -131,13 +394,29 @@ public interface StringBaseCommands { */ CompletableFuture incrBy(String key, long amount); + /** + * Increments the number stored at key by amount. If key + * does not exist, it is set to 0 before performing the operation. + * + * @see valkey.io for details. + * @param key The key to increment its value. + * @param amount The amount to increment. + * @return The value of key after the increment. + * @example + *
{@code
+     * Long num = client.incrBy(gs("key"), 2).get();
+     * assert num == 7L;
+     * }
+ */ + CompletableFuture incrBy(GlideString key, long amount); + /** * Increments the string representing a floating point number stored at key by * amount. By using a negative increment value, the result is that the value stored at * key is decremented. If key does not exist, it is set to 0 before * performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to increment its value. * @param amount The amount to increment. * @return The value of key after the increment. @@ -149,11 +428,29 @@ public interface StringBaseCommands { */ CompletableFuture incrByFloat(String key, double amount); + /** + * Increments the string representing a floating point number stored at key by + * amount. By using a negative increment value, the result is that the value stored at + * key is decremented. If key does not exist, it is set to 0 before + * performing the operation. + * + * @see valkey.io for details. + * @param key The key to increment its value. + * @param amount The amount to increment. + * @return The value of key after the increment. + * @example + *
{@code
+     * Double num = client.incrByFloat(gs("key"), 0.5).get();
+     * assert num == 7.5;
+     * }
+ */ + CompletableFuture incrByFloat(GlideString key, double amount); + /** * Decrements the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to decrement its value. * @return The value of key after the decrement. * @example @@ -164,11 +461,26 @@ public interface StringBaseCommands { */ CompletableFuture decr(String key); + /** + * Decrements the number stored at key by one. If key does not exist, it + * is set to 0 before performing the operation. + * + * @see valkey.io for details. + * @param key The key to decrement its value. + * @return The value of key after the decrement. + * @example + *
{@code
+     * Long num = client.decr(gs("key")).get();
+     * assert num == 4L;
+     * }
+ */ + CompletableFuture decr(GlideString key); + /** * Decrements the number stored at key by amount. If key * does not exist, it is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to decrement its value. * @param amount The amount to decrement. * @return The value of key after the decrement. @@ -180,10 +492,26 @@ public interface StringBaseCommands { */ CompletableFuture decrBy(String key, long amount); + /** + * Decrements the number stored at key by amount. If key + * does not exist, it is set to 0 before performing the operation. + * + * @see valkey.io for details. + * @param key The key to decrement its value. + * @param amount The amount to decrement. + * @return The value of key after the decrement. + * @example + *
{@code
+     * Long num = client.decrBy(gs("key"), 2).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture decrBy(GlideString key, long amount); + /** * Returns the length of the string value stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key to check its length. * @return The length of the string value stored at key.
* If key does not exist, it is treated as an empty string, and the command @@ -200,6 +528,26 @@ public interface StringBaseCommands { */ CompletableFuture strlen(String key); + /** + * Returns the length of the string value stored at key. + * + * @see valkey.io for details. + * @param key The key to check its length. + * @return The length of the string value stored at key.
+ * If key does not exist, it is treated as an empty string, and the command + * returns 0. + * @example + *
{@code
+     * client.set(gs("key"), gs("GLIDE")).get();
+     * Long len = client.strlen(gs("key")).get();
+     * assert len == 5L;
+     *
+     * len = client.strlen(gs("non_existing_key")).get();
+     * assert len == 0L;
+     * }
+ */ + CompletableFuture strlen(GlideString key); + /** * Overwrites part of the string stored at key, starting at the specified * offset, for the entire length of value.
@@ -207,7 +555,7 @@ public interface StringBaseCommands { * the string is padded with zero bytes to make offset fit. Creates the key * if it doesn't exist. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string to update. * @param offset The position in the string where value should be written. * @param value The string written with offset. @@ -222,13 +570,36 @@ public interface StringBaseCommands { */ CompletableFuture setrange(String key, int offset, String value); + /** + * Overwrites part of the GlideString stored at key, starting at the specified + * offset, for the entire length of value.
+ * If the offset is larger than the current length of the GlideString at key + * , the GlideString is padded with zero bytes to make offset fit. Creates the + * key + * if it doesn't exist. + * + * @see valkey.io for details. + * @param key The key of the GlideString to update. + * @param offset The position in the GlideString where value should be written. + * @param value The GlideString written with offset. + * @return The length of the GlideString stored at key after it was modified. + * @example + *
{@code
+     * Long len = client.setrange(gs("key"), 6, gs("GLIDE")).get();
+     * assert len == 11L; // New key was created with length of 11 symbols
+     * GlideString value = client.get(gs("key")).get();
+     * assert value.equals(gs("\0\0\0\0\0\0GLIDE")); // The string was padded with zero bytes
+     * }
+ */ + CompletableFuture setrange(GlideString key, int offset, GlideString value); + /** * Returns the substring of the string value stored at key, determined by the offsets * start and end (both are inclusive). Negative offsets can be used in * order to provide an offset starting from the end of the string. So -1 means the * last character, -2 the penultimate and so forth. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param start The starting offset. * @param end The ending offset. @@ -243,4 +614,505 @@ public interface StringBaseCommands { * }
*/ CompletableFuture getrange(String key, int start, int end); + + /** + * Returns the subGlideString of the GlideString value stored at key, determined by + * the offsets start and end (both are inclusive). Negative offsets can + * be used in order to provide an offset starting from the end of the GlideString. So -1 + * means the last character, -2 the penultimate and so forth. + * + * @see valkey.io for details. + * @param key The key of the GlideString. + * @param start The starting offset. + * @param end The ending offset. + * @return A subGlideString extracted from the value stored at key.. + * @example + *
{@code
+     * client.set(gs("mykey"), gs("This is a GlideString")).get();
+     * GlideString subGlideString = client.getrange(gs("mykey"), 0, 3).get();
+     * assert subGlideString.equals(gs("This"));
+     * GlideString subGlideString = client.getrange(gs("mykey"), -3, -1).get();
+     * assert subGlideString.equals(gs("ing")); // extracted last 3 characters of a GlideString
+     * }
+ */ + CompletableFuture getrange(GlideString key, int start, int end); + + /** + * Appends a value to a key. If key does not exist it is + * created and set as an empty string, so APPEND will be similar to {@see #set} in + * this special case. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param value The value to append. + * @return The length of the string after appending the value. + * @example + *
{@code
+     * Long value = client.append("key", "value").get();
+     * assert value.equals(5L);
+     * }
+ */ + CompletableFuture append(String key, String value); + + /** + * Appends a value to a key. If key does not exist it is + * created and set as an empty string, so APPEND will be similar to {@see #set} in + * this special case. + * + * @see valkey.io for details. + * @param key The key of the string. + * @param value The value to append. + * @return The length of the string after appending the value. + * @example + *
{@code
+     * Long value = client.append(gs("key"), gs("value")).get();
+     * assert value.equals(5L);
+     * }
+ */ + CompletableFuture append(GlideString key, GlideString value); + + /** + * Returns the longest common subsequence between strings stored at key1 and + * key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A String containing the longest common subsequence between the 2 strings. + * An empty String is returned if the keys do not exist or have no common + * subsequences. + * @example + *
{@code
+     * // testKey1 = abcd, testKey2 = axcd
+     * String result = client.lcs("testKey1", "testKey2").get();
+     * assert result.equals("acd");
+     * }
+ */ + CompletableFuture lcs(String key1, String key2); + + /** + * Returns the longest common subsequence between strings stored at key1 and + * key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A String containing the longest common subsequence between the 2 strings. + * An empty String is returned if the keys do not exist or have no common + * subsequences. + * @example + *
{@code
+     * // testKey1 = abcd, testKey2 = axcd
+     * GlideString result = client.lcs(gs("testKey1"), gs("testKey2")).get();
+     * assert result.equals(gs("acd"));
+     * }
+ */ + CompletableFuture lcs(GlideString key1, GlideString key2); + + /** + * Returns the length of the longest common subsequence between strings stored at key1 + * and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return The length of the longest common subsequence between the 2 strings. + * @example + *
{@code
+     * // testKey1 = abcd, testKey2 = axcd
+     * Long result = client.lcsLen("testKey1", "testKey2").get();
+     * assert result.equals(3L);
+     * }
+ */ + CompletableFuture lcsLen(String key1, String key2); + + /** + * Returns the length of the longest common subsequence between strings stored at key1 + * and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return The length of the longest common subsequence between the 2 strings. + * @example + *
{@code
+     * // testKey1 = abcd, testKey2 = axcd
+     * Long result = client.lcsLen(gs("testKey1"), gs("testKey2")).get();
+     * assert result.equals(3L);
+     * }
+ */ + CompletableFuture lcsLen(GlideString key1, GlideString key2); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd123" and key2 + * holds the string "bcdef123" then the sample result would be + *
{@code
+     * new Long[][][] {
+     *      {
+     *          {4L, 6L},
+     *          {5L, 7L}
+     *      },
+     *      {
+     *          {1L, 3L},
+     *          {0L, 2L}
+     *      }
+     *  }
+     * }
+ * The result indicates that the first substring match is "123" in key1 + * at index 4 to 6 which matches the substring in key2 + * at index 5 to 7. And the second substring match is + * "bcd" in key1 at index 1 to 3 which matches + * the substring in key2 at index 0 to 2. + */ + CompletableFuture> lcsIdx(String key1, String key2); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the GlideString gs("abcd123") and key2 + * holds the GlideString gs("bcdef123") then the sample result would be + *
{@code
+     * new Long[][][] {
+     *      {
+     *          {4L, 6L},
+     *          {5L, 7L}
+     *      },
+     *      {
+     *          {1L, 3L},
+     *          {0L, 2L}
+     *      }
+     *  }
+     * }
+ * The result indicates that the first substring match is gs("123") in key1 + * at index 4 to 6 which matches the substring in key2 + * at index 5 to 7. And the second substring match is + * gs("bcd") in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2. + */ + CompletableFuture> lcsIdx(GlideString key1, GlideString key2); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @param minMatchLen The minimum length of matches to include in the result. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd123" and key2 + * holds the string "bcdef123" then the sample result would be + *
{@code
+     * new Long[][][] {
+     *      {
+     *          {4L, 6L},
+     *          {5L, 7L}
+     *      },
+     *      {
+     *          {1L, 3L},
+     *          {0L, 2L}
+     *      }
+     *  }
+     * }
+ * The result indicates that the first substring match is "123" in key1 + * at index 4 to 6 which matches the substring in key2 + * at index 5 to 7. And the second substring match is + * "bcd" in key1 at index 1 to 3 which matches + * the substring in key2 at index 0 to 2. + */ + CompletableFuture> lcsIdx(String key1, String key2, long minMatchLen); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @param minMatchLen The minimum length of matches to include in the result. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the GlideString gs("abcd123") and key2 + * holds the GlideString gs("bcdef123") then the sample result would be + *
{@code
+     * new Long[][][] {
+     *      {
+     *          {4L, 6L},
+     *          {5L, 7L}
+     *      },
+     *      {
+     *          {1L, 3L},
+     *          {0L, 2L}
+     *      }
+     *  }
+     * }
+ * The result indicates that the first substring match is gs("123") in key1 + * at index 4 to 6 which matches the substring in key2 + * at index 5 to 7. And the second substring match is + * gs("bcd") in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2. + */ + CompletableFuture> lcsIdx( + GlideString key1, GlideString key2, long minMatchLen); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd1234" and key2 + * holds the string "bcdef1234" then the sample result would be + *
{@code
+     * new Object[] {
+     *      new Object[] {
+     *          new Long[] {4L, 7L},
+     *          new Long[] {5L, 8L},
+     *          4L},
+     *      new Object[] {
+     *          new Long[] {1L, 3L},
+     *          new Long[] {0L, 2L},
+     *          3L}
+     *      }
+     * }
+ * The result indicates that the first substring match is "1234" in key1 + * at index 4 to 7 which matches the substring in key2 + * at index 5 to 8 and the last element in the array is the + * length of the substring match which is 4. And the second substring match is + * "bcd" in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2 and + * the last element in the array is the length of the substring match which is 3. + */ + CompletableFuture> lcsIdxWithMatchLen(String key1, String key2); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the GlideString gs("abcd1234") and key2 + * holds the GlideString gs("bcdef1234") then the sample result would be + *
{@code
+     * new Object[] {
+     *      new Object[] {
+     *          new Long[] {4L, 7L},
+     *          new Long[] {5L, 8L},
+     *          4L},
+     *      new Object[] {
+     *          new Long[] {1L, 3L},
+     *          new Long[] {0L, 2L},
+     *          3L}
+     *      }
+     * }
+ * The result indicates that the first substring match is gs("1234") in + * key1 + * at index 4 to 7 which matches the substring in key2 + * at index 5 to 8 and the last element in the array is the + * length of the substring match which is 4. And the second substring match is + * gs("bcd") in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2 and + * the last element in the array is the length of the substring match which is 3. + */ + CompletableFuture> lcsIdxWithMatchLen(GlideString key1, GlideString key2); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @param minMatchLen The minimum length of matches to include in the result. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd1234" and key2 + * holds the string "bcdef1234" then the sample result would be + *
{@code
+     * new Object[] {
+     *      new Object[] {
+     *          new Long[] {4L, 7L},
+     *          new Long[] {5L, 8L},
+     *          4L},
+     *      new Object[] {
+     *          new Long[] {1L, 3L},
+     *          new Long[] {0L, 2L},
+     *          3L}
+     *      }
+     * }
+ * The result indicates that the first substring match is "1234" in key1 + * at index 4 to 7 which matches the substring in key2 + * at index 5 to 8 and the last element in the array is the + * length of the substring match which is 4. And the second substring match is + * "bcd" in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2 and + * the last element in the array is the length of the substring match which is 3. + */ + CompletableFuture> lcsIdxWithMatchLen( + String key1, String key2, long minMatchLen); + + /** + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. + * + * @since Valkey 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @param minMatchLen The minimum length of matches to include in the result. + * @return A Map containing the indices of the longest common subsequence between the + * 2 strings and the length of the longest common subsequence. The resulting map contains two + * keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the GlideString gs("abcd1234") and key2 + * holds the GlideString gs("bcdef1234") then the sample result would be + *
{@code
+     * new Object[] {
+     *      new Object[] {
+     *          new Long[] {4L, 7L},
+     *          new Long[] {5L, 8L},
+     *          4L},
+     *      new Object[] {
+     *          new Long[] {1L, 3L},
+     *          new Long[] {0L, 2L},
+     *          3L}
+     *      }
+     * }
+ * The result indicates that the first substring match is gs("1234") in + * key1 + * at index 4 to 7 which matches the substring in key2 + * at index 5 to 8 and the last element in the array is the + * length of the substring match which is 4. And the second substring match is + * gs("bcd") in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2 and + * the last element in the array is the length of the substring match which is 3. + */ + CompletableFuture> lcsIdxWithMatchLen( + GlideString key1, GlideString key2, long minMatchLen); } diff --git a/java/client/src/main/java/glide/api/commands/TransactionsBaseCommands.java b/java/client/src/main/java/glide/api/commands/TransactionsBaseCommands.java new file mode 100644 index 0000000000..199357fdfe --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/TransactionsBaseCommands.java @@ -0,0 +1,64 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.GlideString; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands for the "Transactions Commands" group for standalone and cluster clients. + * + * @see Transactions Commands + */ +public interface TransactionsBaseCommands { + /** + * Marks the given keys to be watched for conditional execution of a transaction. Transactions + * will only execute commands if the watched keys are not modified before execution of the + * transaction. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The keys to watch. + * @return OK. + * @example + *
{@code
+     * assert client.watch(new String[] {"sampleKey"}).get().equals("OK");
+     * transaction.set("sampleKey", "foobar");
+     * Object[] result = client.exec(transaction).get();
+     * assert result != null; // Executes successfully and keys are unwatched.
+     *
+     * assert client.watch(new String[] {"sampleKey"}).get().equals("OK");
+     * transaction.set("sampleKey", "foobar");
+     * assert client.set("sampleKey", "hello world").get().equals("OK");
+     * Object[] result = client.exec(transaction).get();
+     * assert result == null; // null is returned when the watched key is modified before transaction execution.
+     * }
+ */ + CompletableFuture watch(String[] keys); + + /** + * Marks the given keys to be watched for conditional execution of a transaction. Transactions + * will only execute commands if the watched keys are not modified before execution of the + * transaction. + * + * @apiNote When in cluster mode, the command may route to multiple nodes when keys + * map to different hash slots. + * @see valkey.io for details. + * @param keys The keys to watch. + * @return OK. + * @example + *
{@code
+     * assert client.watch(new GlideString[] {gs("sampleKey")}).get().equals("OK");
+     * transaction.set(gs("sampleKey"), gs("foobar"));
+     * Object[] result = client.exec(transaction).get();
+     * assert result != null; // Executes successfully and keys are unwatched.
+     *
+     * assert client.watch(new GlideString[] {gs("sampleKey")}).get().equals("OK");
+     * transaction.set(gs("sampleKey"), gs("foobar"));
+     * assert client.set(gs("sampleKey"), gs("hello world")).get().equals("OK");
+     * Object[] result = client.exec(transaction).get();
+     * assert result == null; // null is returned when the watched key is modified before transaction execution.
+     * }
+ */ + CompletableFuture watch(GlideString[] keys); +} diff --git a/java/client/src/main/java/glide/api/commands/TransactionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/TransactionsClusterCommands.java new file mode 100644 index 0000000000..75204c5938 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/TransactionsClusterCommands.java @@ -0,0 +1,43 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands for the "Transactions Commands" group for cluster clients. + * + * @see Transactions Commands + */ +public interface TransactionsClusterCommands { + /** + * Flushes all the previously watched keys for a transaction. Executing a transaction will + * automatically flush all previously watched keys.
+ * The command will be routed to all primary nodes. + * + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * assert client.watch(new String[] {"sampleKey"}).get().equals("OK");
+     * assert client.unwatch().get().equals("OK"); // Flushes "sampleKey" from watched keys.
+     * }
+ */ + CompletableFuture unwatch(); + + /** + * Flushes all the previously watched keys for a transaction. Executing a transaction will + * automatically flush all previously watched keys. + * + * @see valkey.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * assert client.watch(new String[] {"sampleKey"}).get().equals("OK");
+     * assert client.unwatch(ALL_PRIMARIES).get().equals("OK"); // Flushes "sampleKey" from watched keys for all primary nodes.
+     * }
+ */ + CompletableFuture unwatch(Route route); +} diff --git a/java/client/src/main/java/glide/api/commands/TransactionsCommands.java b/java/client/src/main/java/glide/api/commands/TransactionsCommands.java new file mode 100644 index 0000000000..dc7b113be7 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/TransactionsCommands.java @@ -0,0 +1,25 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands; + +import java.util.concurrent.CompletableFuture; + +/** + * Supports commands for the "Transactions Commands" group for standalone clients. + * + * @see Transactions Commands + */ +public interface TransactionsCommands { + /** + * Flushes all the previously watched keys for a transaction. Executing a transaction will + * automatically flush all previously watched keys. + * + * @see valkey.io for details. + * @return OK. + * @example + *
{@code
+     * assert client.watch(new String[] {"sampleKey"}).get().equals("OK");
+     * assert client.unwatch().get().equals("OK"); // Flushes "sampleKey" from watched keys.
+     * }
+ */ + CompletableFuture unwatch(); +} diff --git a/java/client/src/main/java/glide/api/logging/Logger.java b/java/client/src/main/java/glide/api/logging/Logger.java new file mode 100644 index 0000000000..97f5503487 --- /dev/null +++ b/java/client/src/main/java/glide/api/logging/Logger.java @@ -0,0 +1,267 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.logging; + +import static glide.ffi.resolvers.LoggerResolver.initInternal; +import static glide.ffi.resolvers.LoggerResolver.logInternal; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.NonNull; + +/** + * A singleton class that allows logging which is consistent with logs from the internal rust core. + * The logger can be set up in 2 ways - + * + *
    + *
  1. By calling Logger.init, which configures the logger only if it wasn't + * previously configured. + *
  2. By calling Logger.setLoggerConfig, which replaces the existing configuration, + * and means that new logs will not be saved with the logs that were sent before the call. + *
+ * + * If setLoggerConfig wasn't called, the first log attempt will initialize a new logger + * with default configuration decided by Glide core. + */ +public final class Logger { + @Getter + public enum Level { + DISABLED(-2), + DEFAULT(-1), + ERROR(0), + WARN(1), + INFO(2), + DEBUG(3), + TRACE(4); + + private final int level; + + Level(int level) { + this.level = level; + } + + public static Level fromInt(int i) { + switch (i) { + case 0: + return ERROR; + case 1: + return WARN; + case 2: + return INFO; + case 3: + return DEBUG; + case 4: + return TRACE; + default: + return DEFAULT; + } + } + } + + @Getter private static Level loggerLevel; + + private static void initLogger(@NonNull Level level, String fileName) { + if (level == Level.DISABLED) { + loggerLevel = level; + return; + } + loggerLevel = Level.fromInt(initInternal(level.getLevel(), fileName)); + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the given level. If given a fileName argument, will write the + * logs to files postfixed with fileName. If fileName isn't provided, + * the logs will be written to the console. + * + * @param level Set the logger level to one of + * [DISABLED, DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + * @param fileName If provided, the target of the logs will be the file mentioned. Otherwise, logs + * will be printed to the console. + */ + public static void init(@NonNull Level level, String fileName) { + if (loggerLevel == null) { + initLogger(level, fileName); + } + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the default level decided by Glide core. Given a fileName + * argument, will write the logs to files postfixed with fileName. + * + * @param fileName The target of the logs will be the file mentioned. + */ + public static void init(@NonNull String fileName) { + init(Level.DEFAULT, fileName); + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the default level decided by Glide core. The logs will be written to stdout. + */ + public static void init() { + init(Level.DEFAULT, null); + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the given level. The logs will be written to stdout. + * + * @param level Set the logger level to one of [DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + */ + public static void init(@NonNull Level level) { + init(level, null); + } + + /** + * Logs the provided message if the provided log level is lower than the logger level. This + * overload takes a Supplier to lazily construct the message. + * + * @param level The log level of the provided message. + * @param logIdentifier The log identifier should give the log a context. + * @param messageSupplier The Supplier of the message to log. + */ + public static void log( + @NonNull Level level, + @NonNull String logIdentifier, + @NonNull Supplier messageSupplier) { + if (loggerLevel == null) { + initLogger(Level.DEFAULT, null); + } + + if (level == Level.DISABLED) { + return; + } + + if (!(level.getLevel() <= loggerLevel.getLevel())) { + return; + } + logInternal(level.getLevel(), logIdentifier, messageSupplier.get()); + } + + /** + * Logs the provided message if the provided log level is lower than the logger level. + * + * @param level The log level of the provided message. + * @param logIdentifier The log identifier should give the log a context. + * @param message The message to log. + */ + public static void log( + @NonNull Level level, @NonNull String logIdentifier, @NonNull String message) { + if (loggerLevel == null) { + initLogger(Level.DEFAULT, null); + } + + if (level == Level.DISABLED) { + return; + } + + if (!(level.getLevel() <= loggerLevel.getLevel())) { + return; + } + logInternal(level.getLevel(), logIdentifier, message); + } + + /** + * Logs the provided exception or error if the provided log level is lower than the logger level. + * + * @param level The log level of the provided message. + * @param logIdentifier The log identifier should give the log a context. + * @param message The message to log with the exception. + * @param throwable The exception or error to log. + */ + public static void log( + @NonNull Level level, + @NonNull String logIdentifier, + @NonNull String message, + @NonNull Throwable throwable) { + // TODO: Add the corresponding API to Python and Node.js. + log(level, logIdentifier, () -> message + ": " + prettyPrintException(throwable)); + } + + /** + * Logs the provided exception or error if the provided log level is lower than the logger level. + * + * @param level The log level of the provided message. + * @param logIdentifier The log identifier should give the log a context. + * @param message The message to log with the exception. + * @param throwable The exception or error to log. + */ + public static void log( + @NonNull Level level, + @NonNull String logIdentifier, + @NonNull Supplier message, + @NonNull Throwable throwable) { + // TODO: Add the corresponding API to Python and Node.js. + log(level, logIdentifier, () -> message.get() + ": " + prettyPrintException(throwable)); + } + + private static String prettyPrintException(@NonNull Throwable throwable) { + try (StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter)) { + throwable.printStackTrace(printWriter); + return stringWriter.toString(); + } catch (IOException e) { + // This can't happen with a ByteArrayOutputStream. + return null; + } + } + + /** + * Creates a new logger instance and configure it with the provided log level and file name. + * + * @param level Set the logger level to one of + * [DISABLED, DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + * @param fileName If provided, the target of the logs will be the file mentioned. Otherwise, logs + * will be printed to stdout. + */ + public static void setLoggerConfig(@NonNull Level level, String fileName) { + initLogger(level, fileName); + } + + /** + * Creates a new logger instance and configure it with the provided log level. The logs will be + * written to stdout. + * + * @param level Set the logger level to one of + * [DISABLED, DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + */ + public static void setLoggerConfig(@NonNull Level level) { + setLoggerConfig(level, null); + } + + /** + * Creates a new logger instance and configure it with the provided file name and default log + * level. The logger will filter all logs with a level lower than the default level decided by the + * Glide core. + * + * @param fileName If provided, the target of the logs will be the file mentioned. Otherwise, logs + * will be printed to stdout. + */ + public static void setLoggerConfig(String fileName) { + setLoggerConfig(Level.DEFAULT, fileName); + } + + /** + * Creates a new logger instance. The logger will filter all logs with a level lower than the + * default level decided by Glide core. The logs will be written to stdout. + */ + public static void setLoggerConfig() { + setLoggerConfig(Level.DEFAULT, null); + } +} diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 13c5982b67..ec2ab02b0c 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1,109 +1,234 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; -import static glide.api.models.commands.RangeOptions.createZRangeArgs; -import static glide.utils.ArrayTransformUtils.concatenateArrays; -import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; -import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; -import static redis_request.RedisRequestOuterClass.RequestType.Blpop; -import static redis_request.RedisRequestOuterClass.RequestType.Brpop; -import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; -import static redis_request.RedisRequestOuterClass.RequestType.ClientId; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigGet; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; -import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; -import static redis_request.RedisRequestOuterClass.RequestType.Decr; -import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; -import static redis_request.RedisRequestOuterClass.RequestType.Del; -import static redis_request.RedisRequestOuterClass.RequestType.Echo; -import static redis_request.RedisRequestOuterClass.RequestType.Exists; -import static redis_request.RedisRequestOuterClass.RequestType.Expire; -import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.GetRange; -import static redis_request.RedisRequestOuterClass.RequestType.GetString; -import static redis_request.RedisRequestOuterClass.RequestType.HLen; -import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; -import static redis_request.RedisRequestOuterClass.RequestType.HashDel; -import static redis_request.RedisRequestOuterClass.RequestType.HashExists; -import static redis_request.RedisRequestOuterClass.RequestType.HashGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashGetAll; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.HashMGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashSet; -import static redis_request.RedisRequestOuterClass.RequestType.Hvals; -import static redis_request.RedisRequestOuterClass.RequestType.Incr; -import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.Info; -import static redis_request.RedisRequestOuterClass.RequestType.LInsert; -import static redis_request.RedisRequestOuterClass.RequestType.LLen; -import static redis_request.RedisRequestOuterClass.RequestType.LPop; -import static redis_request.RedisRequestOuterClass.RequestType.LPush; -import static redis_request.RedisRequestOuterClass.RequestType.LPushX; -import static redis_request.RedisRequestOuterClass.RequestType.LRange; -import static redis_request.RedisRequestOuterClass.RequestType.LRem; -import static redis_request.RedisRequestOuterClass.RequestType.LTrim; -import static redis_request.RedisRequestOuterClass.RequestType.LastSave; -import static redis_request.RedisRequestOuterClass.RequestType.Lindex; -import static redis_request.RedisRequestOuterClass.RequestType.MGet; -import static redis_request.RedisRequestOuterClass.RequestType.MSet; -import static redis_request.RedisRequestOuterClass.RequestType.PExpire; -import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.PTTL; -import static redis_request.RedisRequestOuterClass.RequestType.Persist; -import static redis_request.RedisRequestOuterClass.RequestType.PfAdd; -import static redis_request.RedisRequestOuterClass.RequestType.PfCount; -import static redis_request.RedisRequestOuterClass.RequestType.PfMerge; -import static redis_request.RedisRequestOuterClass.RequestType.Ping; -import static redis_request.RedisRequestOuterClass.RequestType.RPop; -import static redis_request.RedisRequestOuterClass.RequestType.RPush; -import static redis_request.RedisRequestOuterClass.RequestType.RPushX; -import static redis_request.RedisRequestOuterClass.RequestType.SAdd; -import static redis_request.RedisRequestOuterClass.RequestType.SCard; -import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.SInter; -import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; -import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMembers; -import static redis_request.RedisRequestOuterClass.RequestType.SMove; -import static redis_request.RedisRequestOuterClass.RequestType.SRem; -import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; -import static redis_request.RedisRequestOuterClass.RequestType.SetRange; -import static redis_request.RedisRequestOuterClass.RequestType.SetString; -import static redis_request.RedisRequestOuterClass.RequestType.Strlen; -import static redis_request.RedisRequestOuterClass.RequestType.TTL; -import static redis_request.RedisRequestOuterClass.RequestType.Time; -import static redis_request.RedisRequestOuterClass.RequestType.Type; -import static redis_request.RedisRequestOuterClass.RequestType.Unlink; -import static redis_request.RedisRequestOuterClass.RequestType.XAdd; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount; -import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; -import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZScore; -import static redis_request.RedisRequestOuterClass.RequestType.Zadd; -import static redis_request.RedisRequestOuterClass.RequestType.Zcard; -import static redis_request.RedisRequestOuterClass.RequestType.Zcount; -import static redis_request.RedisRequestOuterClass.RequestType.Zrange; -import static redis_request.RedisRequestOuterClass.RequestType.Zrank; -import static redis_request.RedisRequestOuterClass.RequestType.Zrem; - +import static command_request.CommandRequestOuterClass.RequestType.Append; +import static command_request.CommandRequestOuterClass.RequestType.BLMPop; +import static command_request.CommandRequestOuterClass.RequestType.BLMove; +import static command_request.CommandRequestOuterClass.RequestType.BLPop; +import static command_request.CommandRequestOuterClass.RequestType.BRPop; +import static command_request.CommandRequestOuterClass.RequestType.BZMPop; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.BitCount; +import static command_request.CommandRequestOuterClass.RequestType.BitField; +import static command_request.CommandRequestOuterClass.RequestType.BitFieldReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.BitOp; +import static command_request.CommandRequestOuterClass.RequestType.BitPos; +import static command_request.CommandRequestOuterClass.RequestType.ClientGetName; +import static command_request.CommandRequestOuterClass.RequestType.ClientId; +import static command_request.CommandRequestOuterClass.RequestType.ConfigGet; +import static command_request.CommandRequestOuterClass.RequestType.ConfigResetStat; +import static command_request.CommandRequestOuterClass.RequestType.ConfigRewrite; +import static command_request.CommandRequestOuterClass.RequestType.ConfigSet; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.CustomCommand; +import static command_request.CommandRequestOuterClass.RequestType.DBSize; +import static command_request.CommandRequestOuterClass.RequestType.Decr; +import static command_request.CommandRequestOuterClass.RequestType.DecrBy; +import static command_request.CommandRequestOuterClass.RequestType.Del; +import static command_request.CommandRequestOuterClass.RequestType.Dump; +import static command_request.CommandRequestOuterClass.RequestType.Echo; +import static command_request.CommandRequestOuterClass.RequestType.Exists; +import static command_request.CommandRequestOuterClass.RequestType.Expire; +import static command_request.CommandRequestOuterClass.RequestType.ExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.ExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.FCall; +import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.FlushAll; +import static command_request.CommandRequestOuterClass.RequestType.FlushDB; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDelete; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDump; +import static command_request.CommandRequestOuterClass.RequestType.FunctionFlush; +import static command_request.CommandRequestOuterClass.RequestType.FunctionList; +import static command_request.CommandRequestOuterClass.RequestType.FunctionLoad; +import static command_request.CommandRequestOuterClass.RequestType.FunctionRestore; +import static command_request.CommandRequestOuterClass.RequestType.FunctionStats; +import static command_request.CommandRequestOuterClass.RequestType.GeoAdd; +import static command_request.CommandRequestOuterClass.RequestType.GeoDist; +import static command_request.CommandRequestOuterClass.RequestType.GeoHash; +import static command_request.CommandRequestOuterClass.RequestType.GeoPos; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearch; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearchStore; +import static command_request.CommandRequestOuterClass.RequestType.Get; +import static command_request.CommandRequestOuterClass.RequestType.GetBit; +import static command_request.CommandRequestOuterClass.RequestType.GetDel; +import static command_request.CommandRequestOuterClass.RequestType.GetEx; +import static command_request.CommandRequestOuterClass.RequestType.GetRange; +import static command_request.CommandRequestOuterClass.RequestType.HDel; +import static command_request.CommandRequestOuterClass.RequestType.HExists; +import static command_request.CommandRequestOuterClass.RequestType.HGet; +import static command_request.CommandRequestOuterClass.RequestType.HGetAll; +import static command_request.CommandRequestOuterClass.RequestType.HIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.HIncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.HKeys; +import static command_request.CommandRequestOuterClass.RequestType.HLen; +import static command_request.CommandRequestOuterClass.RequestType.HMGet; +import static command_request.CommandRequestOuterClass.RequestType.HRandField; +import static command_request.CommandRequestOuterClass.RequestType.HScan; +import static command_request.CommandRequestOuterClass.RequestType.HSet; +import static command_request.CommandRequestOuterClass.RequestType.HSetNX; +import static command_request.CommandRequestOuterClass.RequestType.HStrlen; +import static command_request.CommandRequestOuterClass.RequestType.HVals; +import static command_request.CommandRequestOuterClass.RequestType.Incr; +import static command_request.CommandRequestOuterClass.RequestType.IncrBy; +import static command_request.CommandRequestOuterClass.RequestType.IncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.Info; +import static command_request.CommandRequestOuterClass.RequestType.LCS; +import static command_request.CommandRequestOuterClass.RequestType.LIndex; +import static command_request.CommandRequestOuterClass.RequestType.LInsert; +import static command_request.CommandRequestOuterClass.RequestType.LLen; +import static command_request.CommandRequestOuterClass.RequestType.LMPop; +import static command_request.CommandRequestOuterClass.RequestType.LMove; +import static command_request.CommandRequestOuterClass.RequestType.LPop; +import static command_request.CommandRequestOuterClass.RequestType.LPos; +import static command_request.CommandRequestOuterClass.RequestType.LPush; +import static command_request.CommandRequestOuterClass.RequestType.LPushX; +import static command_request.CommandRequestOuterClass.RequestType.LRange; +import static command_request.CommandRequestOuterClass.RequestType.LRem; +import static command_request.CommandRequestOuterClass.RequestType.LSet; +import static command_request.CommandRequestOuterClass.RequestType.LTrim; +import static command_request.CommandRequestOuterClass.RequestType.LastSave; +import static command_request.CommandRequestOuterClass.RequestType.Lolwut; +import static command_request.CommandRequestOuterClass.RequestType.MGet; +import static command_request.CommandRequestOuterClass.RequestType.MSet; +import static command_request.CommandRequestOuterClass.RequestType.MSetNX; +import static command_request.CommandRequestOuterClass.RequestType.ObjectEncoding; +import static command_request.CommandRequestOuterClass.RequestType.ObjectFreq; +import static command_request.CommandRequestOuterClass.RequestType.ObjectIdleTime; +import static command_request.CommandRequestOuterClass.RequestType.ObjectRefCount; +import static command_request.CommandRequestOuterClass.RequestType.PExpire; +import static command_request.CommandRequestOuterClass.RequestType.PExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.PExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.PTTL; +import static command_request.CommandRequestOuterClass.RequestType.Persist; +import static command_request.CommandRequestOuterClass.RequestType.PfAdd; +import static command_request.CommandRequestOuterClass.RequestType.PfCount; +import static command_request.CommandRequestOuterClass.RequestType.PfMerge; +import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.Publish; +import static command_request.CommandRequestOuterClass.RequestType.RPop; +import static command_request.CommandRequestOuterClass.RequestType.RPush; +import static command_request.CommandRequestOuterClass.RequestType.RPushX; +import static command_request.CommandRequestOuterClass.RequestType.RandomKey; +import static command_request.CommandRequestOuterClass.RequestType.Rename; +import static command_request.CommandRequestOuterClass.RequestType.RenameNX; +import static command_request.CommandRequestOuterClass.RequestType.Restore; +import static command_request.CommandRequestOuterClass.RequestType.SAdd; +import static command_request.CommandRequestOuterClass.RequestType.SCard; +import static command_request.CommandRequestOuterClass.RequestType.SDiff; +import static command_request.CommandRequestOuterClass.RequestType.SDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.SInter; +import static command_request.CommandRequestOuterClass.RequestType.SInterCard; +import static command_request.CommandRequestOuterClass.RequestType.SInterStore; +import static command_request.CommandRequestOuterClass.RequestType.SIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMembers; +import static command_request.CommandRequestOuterClass.RequestType.SMove; +import static command_request.CommandRequestOuterClass.RequestType.SPop; +import static command_request.CommandRequestOuterClass.RequestType.SRandMember; +import static command_request.CommandRequestOuterClass.RequestType.SRem; +import static command_request.CommandRequestOuterClass.RequestType.SScan; +import static command_request.CommandRequestOuterClass.RequestType.SUnion; +import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; +import static command_request.CommandRequestOuterClass.RequestType.Set; +import static command_request.CommandRequestOuterClass.RequestType.SetBit; +import static command_request.CommandRequestOuterClass.RequestType.SetRange; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Strlen; +import static command_request.CommandRequestOuterClass.RequestType.TTL; +import static command_request.CommandRequestOuterClass.RequestType.Time; +import static command_request.CommandRequestOuterClass.RequestType.Touch; +import static command_request.CommandRequestOuterClass.RequestType.Type; +import static command_request.CommandRequestOuterClass.RequestType.Unlink; +import static command_request.CommandRequestOuterClass.RequestType.Wait; +import static command_request.CommandRequestOuterClass.RequestType.XAck; +import static command_request.CommandRequestOuterClass.RequestType.XAdd; +import static command_request.CommandRequestOuterClass.RequestType.XAutoClaim; +import static command_request.CommandRequestOuterClass.RequestType.XClaim; +import static command_request.CommandRequestOuterClass.RequestType.XDel; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreate; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreateConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDelConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDestroy; +import static command_request.CommandRequestOuterClass.RequestType.XGroupSetId; +import static command_request.CommandRequestOuterClass.RequestType.XInfoConsumers; +import static command_request.CommandRequestOuterClass.RequestType.XInfoGroups; +import static command_request.CommandRequestOuterClass.RequestType.XInfoStream; +import static command_request.CommandRequestOuterClass.RequestType.XLen; +import static command_request.CommandRequestOuterClass.RequestType.XPending; +import static command_request.CommandRequestOuterClass.RequestType.XRange; +import static command_request.CommandRequestOuterClass.RequestType.XRead; +import static command_request.CommandRequestOuterClass.RequestType.XReadGroup; +import static command_request.CommandRequestOuterClass.RequestType.XRevRange; +import static command_request.CommandRequestOuterClass.RequestType.XTrim; +import static command_request.CommandRequestOuterClass.RequestType.ZAdd; +import static command_request.CommandRequestOuterClass.RequestType.ZCard; +import static command_request.CommandRequestOuterClass.RequestType.ZCount; +import static command_request.CommandRequestOuterClass.RequestType.ZDiff; +import static command_request.CommandRequestOuterClass.RequestType.ZDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.ZIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.ZInter; +import static command_request.CommandRequestOuterClass.RequestType.ZInterCard; +import static command_request.CommandRequestOuterClass.RequestType.ZInterStore; +import static command_request.CommandRequestOuterClass.RequestType.ZLexCount; +import static command_request.CommandRequestOuterClass.RequestType.ZMPop; +import static command_request.CommandRequestOuterClass.RequestType.ZMScore; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.ZRandMember; +import static command_request.CommandRequestOuterClass.RequestType.ZRange; +import static command_request.CommandRequestOuterClass.RequestType.ZRangeStore; +import static command_request.CommandRequestOuterClass.RequestType.ZRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRem; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByLex; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByScore; +import static command_request.CommandRequestOuterClass.RequestType.ZRevRank; +import static command_request.CommandRequestOuterClass.RequestType.ZScan; +import static command_request.CommandRequestOuterClass.RequestType.ZScore; +import static command_request.CommandRequestOuterClass.RequestType.ZUnion; +import static command_request.CommandRequestOuterClass.RequestType.ZUnionStore; +import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; +import static glide.api.commands.HashBaseCommands.WITH_VALUES_VALKEY_API; +import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_VALKEY_API; +import static glide.api.commands.ServerManagementCommands.VERSION_VALKEY_API; +import static glide.api.commands.SetBaseCommands.SET_LIMIT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.COUNT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.LIMIT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_VALKEY_API; +import static glide.api.commands.StringBaseCommands.IDX_COMMAND_STRING; +import static glide.api.commands.StringBaseCommands.LEN_VALKEY_API; +import static glide.api.commands.StringBaseCommands.MINMATCHLEN_COMMAND_STRING; +import static glide.api.commands.StringBaseCommands.WITHMATCHLEN_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_VALKEY_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_VALKEY_API; +import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; +import static glide.api.models.commands.stream.StreamClaimOptions.JUST_ID_VALKEY_API; +import static glide.api.models.commands.stream.StreamGroupOptions.ENTRIES_READ_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.XInfoStreamOptions.COUNT; +import static glide.api.models.commands.stream.XInfoStreamOptions.FULL; +import static glide.utils.ArrayTransformUtils.flattenAllKeysFollowedByAllValues; +import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArray; +import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArrayValueFirst; +import static glide.utils.ArrayTransformUtils.mapGeoDataToGlideStringArray; + +import command_request.CommandRequestOuterClass.Command; +import command_request.CommandRequestOuterClass.Command.ArgsArray; +import command_request.CommandRequestOuterClass.RequestType; +import command_request.CommandRequestOuterClass.Transaction; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.GetExOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.LInsertOptions.InsertPosition; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; @@ -116,79 +241,131 @@ import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.RangeOptions.ScoreRange; import glide.api.models.commands.RangeOptions.ScoredRangeQuery; +import glide.api.models.commands.RestoreOptions; +import glide.api.models.commands.ScoreFilter; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.ConditionalSet; import glide.api.models.commands.SetOptions.SetOptionsBuilder; -import glide.api.models.commands.StreamAddOptions; -import glide.api.models.commands.StreamAddOptions.StreamAddOptionsBuilder; -import glide.api.models.commands.ZaddOptions; +import glide.api.models.commands.WeightAggregateOptions; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.WeightAggregateOptions.KeyArrayBinary; +import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; +import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeysBinary; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeysBinary; +import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.geospatial.GeoAddOptions; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin.CoordOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.MemberOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.SearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamPendingOptions; +import glide.api.models.commands.stream.StreamRange; +import glide.api.models.commands.stream.StreamRange.IdBound; +import glide.api.models.commands.stream.StreamRange.InfRangeBound; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions; +import glide.api.models.configuration.ReadFrom; +import glide.managers.CommandManager; +import glide.utils.ArgsBuilder; import java.util.Map; import lombok.Getter; import lombok.NonNull; -import org.apache.commons.lang3.ArrayUtils; -import redis_request.RedisRequestOuterClass.Command; -import redis_request.RedisRequestOuterClass.Command.ArgsArray; -import redis_request.RedisRequestOuterClass.RequestType; -import redis_request.RedisRequestOuterClass.Transaction; /** - * Base class encompassing shared commands for both standalone and cluster mode implementations in a - * transaction. Transactions allow the execution of a group of commands in a single step. + * Base class encompassing shared commands for both standalone and cluster server installations. + * Transactions allow the execution of a group of commands in a single step. * - *

Command Response: An array of command responses is returned by the client exec command, in the - * order they were given. Each element in the array represents a command given to the transaction. - * The response for each command depends on the executed Redis command. Specific response types are - * documented alongside each method. + *

Transaction Response: An array of command responses is returned by the client + * exec command, in the order they were given. Each element in the array represents a + * command given to the transaction. The response for each command depends on the executed Valkey + * command. Specific response types are documented alongside each method. * - * @param child typing for chaining method calls + * @param child typing for chaining method calls. */ @Getter public abstract class BaseTransaction> { - /** Command class to send a single request to Redis. */ + /** Command class to send a single request to Valkey. */ protected final Transaction.Builder protobufTransaction = Transaction.newBuilder(); + /** + * Flag whether transaction commands may return binary data.
+ * If set to true, all commands in this transaction return {@link GlideString} + * instead of {@link String}. + */ + protected boolean binaryOutput = false; + + /** Sets {@link #binaryOutput} to true. */ + public T withBinaryOutput() { + binaryOutput = true; + return getThis(); + } + protected abstract T getThis(); /** * Executes a single command, without checking inputs. Every part of the command, including * subcommands, should be added as a separate value in args. * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @apiNote See Glide + * Wiki for details on the restrictions and limitations of the custom command API. * @param args Arguments for the custom command. - * @return A response from Redis with an Object. - * @remarks This function should only be used for single-response commands. Commands that don't - * return response (such as SUBSCRIBE), or that return potentially more than a single - * response (such as XREAD), or that change the client's behavior (such as entering - * pub/sub mode on RESP2 connections) shouldn't be called using - * this function. - * @example Returns a list of all pub/sub clients: - *

{@code
-     * Object result = client.customCommand(new String[]{ "CLIENT", "LIST", "TYPE", "PUBSUB" }).get();
-     * }
+ * @return Command Response - A response from the server with an Object. */ - public T customCommand(String[] args) { - ArgsArray commandArgs = buildArgs(args); - protobufTransaction.addCommands(buildCommand(CustomCommand, commandArgs)); + public T customCommand(ArgType[] args) { + checkTypeOrThrow(args); + protobufTransaction.addCommands(buildCommand(CustomCommand, newArgsBuilder().add(args))); return getThis(); } /** * Echoes the provided message back. * - * @see valkey.io for details. * @param message The message to be echoed back. * @return Command Response - The provided message. */ - public T echo(@NonNull String message) { - ArgsArray commandArgs = buildArgs(message); - protobufTransaction.addCommands(buildCommand(Echo, commandArgs)); + public T echo(@NonNull ArgType message) { + checkTypeOrThrow(message); + protobufTransaction.addCommands(buildCommand(Echo, newArgsBuilder().add(message))); return getThis(); } /** - * Pings the Redis server. + * Pings the server. * - * @see redis.io for details. - * @return Command Response - A response from Redis with a String. + * @see valkey.io for details. + * @return Command Response - A response from the server with a String. */ public T ping() { protobufTransaction.addCommands(buildCommand(Ping)); @@ -196,23 +373,24 @@ public T ping() { } /** - * Pings the Redis server. + * Pings the server. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param msg The ping argument that will be returned. - * @return Command Response - A response from Redis with a String. + * @return Command Response - A response from the server with a String. */ - public T ping(@NonNull String msg) { - ArgsArray commandArgs = buildArgs(msg); - protobufTransaction.addCommands(buildCommand(Ping, commandArgs)); + public T ping(@NonNull ArgType msg) { + checkTypeOrThrow(msg); + protobufTransaction.addCommands(buildCommand(Ping, newArgsBuilder().add(msg))); return getThis(); } /** - * Gets information and statistics about the Redis server using the {@link Section#DEFAULT} - * option. + * Gets information and statistics about the server using the {@link Section#DEFAULT} option. * - * @see redis.io for details. + * @see valkey.io for details. * @return Command Response - A String with server info. */ public T info() { @@ -221,16 +399,15 @@ public T info() { } /** - * Gets information and statistics about the Redis server. + * Gets information and statistics about the server. * - * @see redis.io for details. + * @see valkey.io for details. * @param options A list of {@link Section} values specifying which sections of information to * retrieve. When no parameter is provided, the {@link Section#DEFAULT} option is assumed. * @return Command Response - A String containing the requested {@link Section}s. */ public T info(@NonNull InfoOptions options) { - ArgsArray commandArgs = buildArgs(options.toArgs()); - protobufTransaction.addCommands(buildCommand(Info, commandArgs)); + protobufTransaction.addCommands(buildCommand(Info, newArgsBuilder().add(options.toArgs()))); return getThis(); } @@ -238,48 +415,108 @@ public T info(@NonNull InfoOptions options) { * Removes the specified keys from the database. A key is ignored if it does not * exist. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param keys The keys we wanted to remove. * @return Command Response - The number of keys that were removed. */ - public T del(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(keys); - protobufTransaction.addCommands(buildCommand(Del, commandArgs)); + public T del(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(Del, newArgsBuilder().add(keys))); return getThis(); } /** - * Gets the value associated with the given key, or null if no such value exists. + * Gets the value associated with the given key, or null if no such value exists. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to retrieve from the database. * @return Command Response - If key exists, returns the value of - * key as a String. Otherwise, return null. + * key
as a String. Otherwise, return null. + */ + public T get(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Get, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Gets a string value associated with the given key and deletes the key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @return Command Response - If key exists, returns the value of + * key. Otherwise, return null. */ - public T get(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(GetString, commandArgs)); + public T getdel(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(GetDel, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Gets the value associated with the given key. + * + * @since Valkey 6.2.0. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @return Command Response - If key exists, return the value of the + * key. Otherwise, return null. + */ + public T getex(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(GetEx, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Gets the value associated with the given key. + * + * @since Valkey 6.2.0. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to retrieve from the database. + * @param options The {@link GetExOptions} options. + * @return Command Response - If key exists, return the value of the + * key. Otherwise, return null. + */ + public T getex(@NonNull ArgType key, @NonNull GetExOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(GetEx, newArgsBuilder().add(key).add(options.toArgs()))); return getThis(); } /** * Sets the given key with the given value. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to store. * @param value The value to store with the given key. - * @return Command Response - A response from Redis. + * @return Command Response - OK. */ - public T set(@NonNull String key, @NonNull String value) { - ArgsArray commandArgs = buildArgs(key, value); - protobufTransaction.addCommands(buildCommand(SetString, commandArgs)); + public T set(@NonNull ArgType key, @NonNull ArgType value) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Set, newArgsBuilder().add(key).add(value))); return getThis(); } /** * Sets the given key with the given value. Return value is dependent on the passed options. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param key The key to store. * @param value The value to store with the given key. * @param options The Set options. @@ -289,42 +526,75 @@ public T set(@NonNull String key, @NonNull String value) { * {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} conditions, return null. * Otherwise, return OK. */ - public T set(@NonNull String key, @NonNull String value, @NonNull SetOptions options) { - ArgsArray commandArgs = - buildArgs(ArrayUtils.addAll(new String[] {key, value}, options.toArgs())); + public T set( + @NonNull ArgType key, @NonNull ArgType value, @NonNull SetOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Set, newArgsBuilder().add(key).add(value).add(options.toArgs()))); + return getThis(); + } - protobufTransaction.addCommands(buildCommand(SetString, commandArgs)); + /** + * Appends a value to a key. If key does not exist it is + * created and set as an empty string, so APPEND will be similar to {@link #set} in + * this special case. + * + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @param key The key of the string. + * @param value The value to append. + * @return Command Response - The length of the string after appending the value. + */ + public T append(@NonNull ArgType key, @NonNull ArgType value) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Append, newArgsBuilder().add(key).add(value))); return getThis(); } /** * Retrieves the values of multiple keys. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param keys A list of keys to retrieve values for. - * @return Command Response - An array of values corresponding to the provided keys. - *
+ * @return Command Response - An array of values corresponding to the provided + * keys.
* If a keyis not found, its corresponding value in the list will be null * . */ - public T mget(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(keys); - protobufTransaction.addCommands(buildCommand(MGet, commandArgs)); + public T mget(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(MGet, newArgsBuilder().add(keys))); return getThis(); } /** * Sets multiple keys to multiple values in a single operation. * - * @see redis.io for details. + * @see valkey.io for details. * @param keyValueMap A key-value map consisting of keys and their respective values to set. * @return Command Response - Always OK. */ - public T mset(@NonNull Map keyValueMap) { - String[] args = convertMapToKeyValueStringArray(keyValueMap); - ArgsArray commandArgs = buildArgs(args); + public T mset(@NonNull Map keyValueMap) { + GlideString[] args = flattenMapToGlideStringArray(keyValueMap); + protobufTransaction.addCommands(buildCommand(MSet, newArgsBuilder().add(args))); + return getThis(); + } - protobufTransaction.addCommands(buildCommand(MSet, commandArgs)); + /** + * Sets multiple keys to multiple values in a single operation. Performs no operation at all even + * if just a single key already exists. + * + * @see valkey.io for details. + * @param keyValueMap A key-value map consisting of keys and their respective values to set. + * @return Command Response - true if all keys were set, false if no key + * was set. + */ + public T msetnx(@NonNull Map keyValueMap) { + GlideString[] args = flattenMapToGlideStringArray(keyValueMap); + protobufTransaction.addCommands(buildCommand(MSetNX, newArgsBuilder().add(args))); return getThis(); } @@ -332,13 +602,15 @@ public T mset(@NonNull Map keyValueMap) { * Increments the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param key The key to increment its value. * @return Command Response - The value of key after the increment. */ - public T incr(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Incr, commandArgs)); + public T incr(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Incr, newArgsBuilder().add(key))); return getThis(); } @@ -346,31 +618,36 @@ public T incr(@NonNull String key) { * Increments the number stored at key by amount. If key * does not exist, it is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param key The key to increment its value. * @param amount The amount to increment. * @return Command Response - The value of key after the increment. */ - public T incrBy(@NonNull String key, long amount) { - ArgsArray commandArgs = buildArgs(key, Long.toString(amount)); - protobufTransaction.addCommands(buildCommand(IncrBy, commandArgs)); + public T incrBy(@NonNull ArgType key, long amount) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(IncrBy, newArgsBuilder().add(key).add(amount))); return getThis(); } /** * Increments the string representing a floating point number stored at key by - * amount. By using a negative increment value, the result is that the value stored at + * amount
. By using a negative increment value, the result is that the value stored at * key is decremented. If key does not exist, it is set to 0 before * performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param key The key to increment its value. * @param amount The amount to increment. * @return Command Response - The value of key after the increment. */ - public T incrByFloat(@NonNull String key, double amount) { - ArgsArray commandArgs = buildArgs(key, Double.toString(amount)); - protobufTransaction.addCommands(buildCommand(IncrByFloat, commandArgs)); + public T incrByFloat(@NonNull ArgType key, double amount) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(IncrByFloat, newArgsBuilder().add(key).add(amount))); return getThis(); } @@ -378,13 +655,15 @@ public T incrByFloat(@NonNull String key, double amount) { * Decrements the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param key The key to decrement its value. * @return Command Response - The value of key after the decrement. */ - public T decr(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Decr, commandArgs)); + public T decr(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Decr, newArgsBuilder().add(key))); return getThis(); } @@ -392,49 +671,56 @@ public T decr(@NonNull String key) { * Decrements the number stored at key by amount. If key * does not exist, it is set to 0 before performing the operation. * - * @see redis.io for details. + * @see valkey.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. * @param key The key to decrement its value. * @param amount The amount to decrement. * @return Command Response - The value of key after the decrement. */ - public T decrBy(@NonNull String key, long amount) { - ArgsArray commandArgs = buildArgs(key, Long.toString(amount)); - protobufTransaction.addCommands(buildCommand(DecrBy, commandArgs)); + public T decrBy(@NonNull ArgType key, long amount) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(DecrBy, newArgsBuilder().add(key).add(amount))); return getThis(); } /** * Returns the length of the string value stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to check its length. * @return Command Response - The length of the string value stored at key.
* If key does not exist, it is treated as an empty string, and the command * returns 0. */ - public T strlen(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Strlen, commandArgs)); + public T strlen(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Strlen, newArgsBuilder().add(key))); return getThis(); } /** * Overwrites part of the string stored at key, starting at the specified - * offset, for the entire length of value.
+ * offset, for the entire length of value.
* If the offset is larger than the current length of the string at key, * the string is padded with zero bytes to make offset fit. Creates the key * if it doesn't exist. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the string to update. * @param offset The position in the string where value should be written. * @param value The string written with offset. * @return Command Response - The length of the string stored at key after it was * modified. */ - public T setrange(@NonNull String key, int offset, @NonNull String value) { - ArgsArray commandArgs = buildArgs(key, Integer.toString(offset), value); - protobufTransaction.addCommands(buildCommand(SetRange, commandArgs)); + public T setrange(@NonNull ArgType key, int offset, @NonNull ArgType value) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SetRange, newArgsBuilder().add(key).add(offset).add(value))); return getThis(); } @@ -444,47 +730,54 @@ public T setrange(@NonNull String key, int offset, @NonNull String value) { * order to provide an offset starting from the end of the string. So -1 means the * last character, -2 the penultimate and so forth. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the string. * @param start The starting offset. * @param end The ending offset. * @return Command Response - A substring extracted from the value stored at key. */ - public T getrange(@NonNull String key, int start, int end) { - ArgsArray commandArgs = buildArgs(key, Integer.toString(start), Integer.toString(end)); - protobufTransaction.addCommands(buildCommand(GetRange, commandArgs)); + public T getrange(@NonNull ArgType key, int start, int end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(GetRange, newArgsBuilder().add(key).add(start).add(end))); return getThis(); } /** * Retrieves the value associated with field in the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field in the hash stored at key to retrieve from the database. * @return Command Response - The value associated with field, or null * when field is not present in the hash or key does not exist. */ - public T hget(@NonNull String key, @NonNull String field) { - ArgsArray commandArgs = buildArgs(key, field); - protobufTransaction.addCommands(buildCommand(HashGet, commandArgs)); + public T hget(@NonNull ArgType key, @NonNull ArgType field) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HGet, newArgsBuilder().add(key).add(field))); return getThis(); } /** * Sets the specified fields to their respective values in the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param fieldValueMap A field-value map consisting of fields and their corresponding values to * be set in the hash stored at the specified key. * @return Command Response - The number of fields that were added. */ - public T hset(@NonNull String key, @NonNull Map fieldValueMap) { - ArgsArray commandArgs = - buildArgs(ArrayUtils.addFirst(convertMapToKeyValueStringArray(fieldValueMap), key)); - - protobufTransaction.addCommands(buildCommand(HashSet, commandArgs)); + public T hset(@NonNull ArgType key, @NonNull Map fieldValueMap) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + HSet, newArgsBuilder().add(key).add(flattenMapToGlideStringArray(fieldValueMap)))); return getThis(); } @@ -494,16 +787,19 @@ public T hset(@NonNull String key, @NonNull Map fieldValueMap) { * If key does not exist, a new key holding a hash is created.
* If field already exists, this operation has no effect. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field to set the value for. * @param value The value to set. * @return Command Response - true if the field was set, false if the * field already existed and was not set. */ - public T hsetnx(@NonNull String key, @NonNull String field, @NonNull String value) { - ArgsArray commandArgs = buildArgs(key, field, value); - protobufTransaction.addCommands(buildCommand(HSetNX, commandArgs)); + public T hsetnx(@NonNull ArgType key, @NonNull ArgType field, @NonNull ArgType value) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(HSetNX, newArgsBuilder().add(key).add(field).add(value))); return getThis(); } @@ -511,94 +807,106 @@ public T hsetnx(@NonNull String key, @NonNull String field, @NonNull String valu * Removes the specified fields from the hash stored at key. Specified fields that do * not exist within this hash are ignored. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param fields The fields to remove from the hash stored at key. * @return Command Response - The number of fields that were removed from the hash, not including * specified but non-existing fields.
* If key does not exist, it is treated as an empty hash and it returns 0.
*/ - public T hdel(@NonNull String key, @NonNull String[] fields) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(fields, key)); - protobufTransaction.addCommands(buildCommand(HashDel, commandArgs)); + public T hdel(@NonNull ArgType key, @NonNull ArgType[] fields) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HDel, newArgsBuilder().add(key).add(fields))); return getThis(); } /** * Returns the number of fields contained in the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @return Command Response - The number of fields in the hash, or 0 when the key * does not exist.
* If key holds a value that is not a hash, an error is returned. */ - public T hlen(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(HLen, commandArgs)); + public T hlen(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HLen, newArgsBuilder().add(key))); return getThis(); } /** * Returns all values in the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @return Command Response - An array of values in the hash, or an empty array * when the key does not exist. */ - public T hvals(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Hvals, commandArgs)); + public T hvals(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HVals, newArgsBuilder().add(key))); return getThis(); } /** * Returns the values associated with the specified fields in the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param fields The fields in the hash stored at key to retrieve from the database. * @return Command Response - An array of values associated with the given fields, in the same * order as they are requested.
- * For every field that does not exist in the hash, a null value is returned.
+ * For every field that does not exist in the hash, a null value is returned.
* If key does not exist, it is treated as an empty hash, and it returns an array - * of null values.
+ * of null values.
*/ - public T hmget(@NonNull String key, @NonNull String[] fields) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(fields, key)); - protobufTransaction.addCommands(buildCommand(HashMGet, commandArgs)); + public T hmget(@NonNull ArgType key, @NonNull ArgType[] fields) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HMGet, newArgsBuilder().add(key).add(fields))); return getThis(); } /** * Returns if field is an existing field in the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field to check in the hash stored at key. - * @return Command Response - True if the hash contains the specified field. If the - * hash does not contain the field, or if the key does not exist, it returns False + * @return Command Response - true if the hash contains the specified field. If the + * hash does not contain the field, or if the key does not exist, it returns false * . */ - public T hexists(@NonNull String key, @NonNull String field) { - ArgsArray commandArgs = buildArgs(key, field); - protobufTransaction.addCommands(buildCommand(HashExists, commandArgs)); + public T hexists(@NonNull ArgType key, @NonNull ArgType field) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HExists, newArgsBuilder().add(key).add(field))); return getThis(); } /** * Returns all fields and values of the hash stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @return Command Response - A Map of fields and their values stored in the hash. * Every field name in the map is associated with its corresponding value.
* If key does not exist, it returns an empty map. */ - public T hgetall(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(HashGetAll, commandArgs)); + public T hgetall(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HGetAll, newArgsBuilder().add(key))); return getThis(); } @@ -608,7 +916,9 @@ public T hgetall(@NonNull String key) { * hash stored at key is decremented. If field or key does * not exist, it is set to 0 before performing the operation. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field in the hash stored at key to increment or decrement its * value. @@ -617,9 +927,10 @@ public T hgetall(@NonNull String key) { * @return Command Response - The value of field in the hash stored at key * after the increment or decrement. */ - public T hincrBy(@NonNull String key, @NonNull String field, long amount) { - ArgsArray commandArgs = buildArgs(key, field, Long.toString(amount)); - protobufTransaction.addCommands(buildCommand(HashIncrBy, commandArgs)); + public T hincrBy(@NonNull ArgType key, @NonNull ArgType field, long amount) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(HIncrBy, newArgsBuilder().add(key).add(field).add(amount))); return getThis(); } @@ -627,10 +938,12 @@ public T hincrBy(@NonNull String key, @NonNull String field, long amount) { * Increments the string representing a floating point number stored at field in the * hash stored at key by increment. By using a negative increment value, the value * stored at field in the hash stored at key is decremented. If - * field or key does not exist, it is set to 0 before performing the + * field
or key does not exist, it is set to 0 before performing the * operation. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the hash. * @param field The field in the hash stored at key to increment or decrement its * value. @@ -639,26 +952,125 @@ public T hincrBy(@NonNull String key, @NonNull String field, long amount) { * @return Command Response - The value of field in the hash stored at key * after the increment or decrement. */ - public T hincrByFloat(@NonNull String key, @NonNull String field, double amount) { - ArgsArray commandArgs = buildArgs(key, field, Double.toString(amount)); - protobufTransaction.addCommands(buildCommand(HashIncrByFloat, commandArgs)); + public T hincrByFloat(@NonNull ArgType key, @NonNull ArgType field, double amount) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(HIncrByFloat, newArgsBuilder().add(key).add(field).add(amount))); + return getThis(); + } + + /** + * Returns all field names in the hash stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details + * @param key The key of the hash. + * @return Command Response - An array of field names in the hash, or an + * empty array when the key does not exist. + */ + public T hkeys(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HKeys, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the string length of the value associated with field in the hash stored at + * key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash. + * @return Command Response - The string length or 0 if field or + * key does not exist. + */ + public T hstrlen(@NonNull ArgType key, @NonNull ArgType field) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HStrlen, newArgsBuilder().add(key).add(field))); + return getThis(); + } + + /** + * Returns a random field name from the hash value stored at key. + * + * @since Valkey 6.2 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the hash. + * @return Command Response - A random field name from the hash stored at key, or + * null when the key does not exist. + */ + public T hrandfield(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HRandField, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Retrieves up to count random field names from the hash value stored at key + * . + * + * @since Valkey 6.2 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return Command Response - An array of random field names from the hash stored at + * key, or an empty array when the key does not exist. + */ + public T hrandfieldWithCount(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HRandField, newArgsBuilder().add(key).add(count))); + return getThis(); + } + + /** + * Retrieves up to count random field names along with their values from the hash + * value stored at key. + * + * @since Valkey 6.2 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return Command Response - A 2D array of [fieldName, + * value] arrays, where fieldName is a random field name + * from the hash and value is the associated value of the field name.
+ * If the hash does not exist or is empty, the response will be an empty array. + */ + public T hrandfieldWithCountWithValues(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(HRandField, newArgsBuilder().add(key).add(count).add(WITH_VALUES_VALKEY_API))); return getThis(); } /** * Inserts all the specified values at the head of the list stored at key. - * elements are inserted one after the other to the head of the list, from the leftmost + * elements are inserted one after the other to the head of the list, from the leftmost * element to the rightmost element. If key does not exist, it is created as an empty * list before performing the push operations. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param elements The elements to insert at the head of the list stored at key. * @return Command Response - The length of the list after the push operations. */ - public T lpush(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - protobufTransaction.addCommands(buildCommand(LPush, commandArgs)); + public T lpush(@NonNull ArgType key, @NonNull ArgType[] elements) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LPush, newArgsBuilder().add(key).add(elements))); return getThis(); } @@ -666,14 +1078,108 @@ public T lpush(@NonNull String key, @NonNull String[] elements) { * Removes and returns the first elements of the list stored at key. The command pops * a single element from the beginning of the list. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @return Command Response - The value of the first element.
- * If key does not exist, null will be returned. + * If key does not exist, null will be returned. + */ + public T lpop(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LPop, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the index of the first occurrence of element inside the list specified by + * key. If no match is found, null is returned. + * + * @since Valkey 6.0.6. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @return Command Response - The index of the first occurrence of element, or + * null if element is not in the list. + */ + public T lpos(@NonNull ArgType key, @NonNull ArgType element) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LPos, newArgsBuilder().add(key).add(element))); + return getThis(); + } + + /** + * Returns the index of an occurrence of element within a list based on the given + * options. If no match is found, null is returned. + * + * @since Valkey 6.0.6. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param options The LPos options. + * @return Command Response - The index of element, or null if + * element is not in the list. + */ + public T lpos( + @NonNull ArgType key, @NonNull ArgType element, @NonNull LPosOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(LPos, newArgsBuilder().add(key).add(element).add(options.toArgs()))); + return getThis(); + } + + /** + * Returns an array of indices of matching elements within a list. + * + * @since Valkey 6.0.6. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param count The number of matches wanted. + * @return Command Response - An array that holds the indices of the matching + * elements within the list. + */ + public T lposCount(@NonNull ArgType key, @NonNull ArgType element, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + LPos, newArgsBuilder().add(key).add(element).add(COUNT_VALKEY_API).add(count))); + return getThis(); + } + + /** + * Returns an array of indices of matching elements within a list based on the given + * options. If no match is found, an empty arrayis returned. + * + * @since Valkey 6.0.6. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The name of the list. + * @param element The value to search for within the list. + * @param count The number of matches wanted. + * @param options The LPos options. + * @return Command Response - An array that holds the indices of the matching + * elements within the list. */ - public T lpop(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(LPop, commandArgs)); + public T lposCount( + @NonNull ArgType key, @NonNull ArgType element, long count, @NonNull LPosOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + LPos, + newArgsBuilder() + .add(key) + .add(element) + .add(COUNT_VALKEY_API) + .add(count) + .add(options.toArgs()))); return getThis(); } @@ -681,16 +1187,18 @@ public T lpop(@NonNull String key) { * Removes and returns up to count elements of the list stored at key, * depending on the list's length. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param count The count of the elements to pop from the list. * @return Command Response - An array of the popped elements will be returned depending on the * list's length.
- * If key does not exist, null will be returned. + * If key does not exist, null will be returned. */ - public T lpopCount(@NonNull String key, long count) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count)); - protobufTransaction.addCommands(buildCommand(LPop, commandArgs)); + public T lpopCount(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LPop, newArgsBuilder().add(key).add(count))); return getThis(); } @@ -702,7 +1210,9 @@ public T lpopCount(@NonNull String key, long count) { * -1 being the last element of the list, -2 being the penultimate, and * so on. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param start The starting point of the range. * @param end The end of the range. @@ -713,9 +1223,10 @@ public T lpopCount(@NonNull String key, long count) { * end of the list.
* If key does not exist an empty array will be returned. */ - public T lrange(@NonNull String key, long start, long end) { - ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); - protobufTransaction.addCommands(buildCommand(LRange, commandArgs)); + public T lrange(@NonNull ArgType key, long start, long end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(LRange, newArgsBuilder().add(key).add(start).add(end))); return getThis(); } @@ -726,7 +1237,9 @@ public T lrange(@NonNull String key, long start, long end) { * the list. Here, -1 means the last element, -2 means the penultimate * and so forth. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param index The index of the element in the list to retrieve. * @return Command Response - The element at index in the list stored at key @@ -734,21 +1247,24 @@ public T lrange(@NonNull String key, long start, long end) { * If index is out of range or if key does not exist, null * is returned. */ - public T lindex(@NonNull String key, long index) { - ArgsArray commandArgs = buildArgs(key, Long.toString(index)); - - protobufTransaction.addCommands(buildCommand(Lindex, commandArgs)); + public T lindex(@NonNull ArgType key, long index) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LIndex, newArgsBuilder().add(key).add(index))); return getThis(); } /** - * Trims an existing list so that it will contain only the specified range of elements specified.
- * The offsets start and end are zero-based indexes, with 0 being the - * first element of the list,
1 being the next element and so on.
+ * Trims an existing list so that it will contain only the specified range of elements specified. + *
+ * The offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on.
* These offsets can also be negative numbers indicating offsets starting at the end of the list, - * with -1 being the last element of the list, -2 being the penultimate, and so on. + * with -1 being the last element of the list, -2 being the penultimate, + * and so on. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param start The starting point of the range. * @param end The end of the range. @@ -759,25 +1275,27 @@ public T lindex(@NonNull String key, long index) { * element of the list.
* If key does not exist, OK will be returned without changes to the database. */ - public T ltrim(@NonNull String key, long start, long end) { - ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); - protobufTransaction.addCommands(buildCommand(LTrim, commandArgs)); + public T ltrim(@NonNull ArgType key, long start, long end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(LTrim, newArgsBuilder().add(key).add(start).add(end))); return getThis(); } /** * Returns the length of the list stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @return Command Response - The length of the list at key.
* If key does not exist, it is interpreted as an empty list and 0 * is returned. */ - public T llen(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - - protobufTransaction.addCommands(buildCommand(LLen, commandArgs)); + public T llen(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LLen, newArgsBuilder().add(key))); return getThis(); } @@ -791,16 +1309,19 @@ public T llen(@NonNull String key) { * If count is 0 or count is greater than the occurrences of elements * equal to element, it removes all elements equal to element. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param count The count of the occurrences of elements equal to element to remove. * @param element The element to remove from the list. * @return Command Response - The number of the removed elements.
* If key does not exist, 0 is returned. */ - public T lrem(@NonNull String key, long count, @NonNull String element) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count), element); - protobufTransaction.addCommands(buildCommand(LRem, commandArgs)); + public T lrem(@NonNull ArgType key, long count, @NonNull ArgType element) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(LRem, newArgsBuilder().add(key).add(count).add(element))); return getThis(); } @@ -810,14 +1331,16 @@ public T lrem(@NonNull String key, long count, @NonNull String element) { * leftmost element to the rightmost element. If key does not exist, it is created as * an empty list before performing the push operations. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @param elements The elements to insert at the tail of the list stored at key. * @return Command Response - The length of the list after the push operations. */ - public T rpush(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - protobufTransaction.addCommands(buildCommand(RPush, commandArgs)); + public T rpush(@NonNull ArgType key, @NonNull ArgType[] elements) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(RPush, newArgsBuilder().add(key).add(elements))); return getThis(); } @@ -825,14 +1348,16 @@ public T rpush(@NonNull String key, @NonNull String[] elements) { * Removes and returns the last elements of the list stored at key.
* The command pops a single element from the end of the list. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the list. * @return Command Response - The value of the last element.
* If key does not exist, null will be returned. */ - public T rpop(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); + public T rpop(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(RPop, newArgsBuilder().add(key))); return getThis(); } @@ -840,15 +1365,17 @@ public T rpop(@NonNull String key) { * Removes and returns up to count elements from the list stored at key, * depending on the list's length. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param count The count of the elements to pop from the list. * @return Command Response - An array of popped elements will be returned depending on the list's * length.
* If key does not exist, null will be returned. */ - public T rpopCount(@NonNull String key, long count) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count)); - protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); + public T rpopCount(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(RPop, newArgsBuilder().add(key).add(count))); return getThis(); } @@ -856,7 +1383,9 @@ public T rpopCount(@NonNull String key, long count) { * Adds specified members to the set stored at key. Specified members that are * already a member of this set are ignored. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key where members will be added to its set. * @param members A list of members to add to the set stored at key. * @return Command Response - The number of members that were added to the set, excluding members @@ -864,25 +1393,27 @@ public T rpopCount(@NonNull String key, long count) { * @remarks If key does not exist, a new set is created before adding members * . */ - public T sadd(@NonNull String key, @NonNull String[] members) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); - protobufTransaction.addCommands(buildCommand(SAdd, commandArgs)); + public T sadd(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SAdd, newArgsBuilder().add(key).add(members))); return getThis(); } /** * Returns if member is a member of the set stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the set. * @param member The member to check for existence in the set. * @return Command Response - true if the member exists in the set, false * otherwise. If key doesn't exist, it is treated as an empty set * and the command returns false. */ - public T sismember(@NonNull String key, @NonNull String member) { - ArgsArray commandArgs = buildArgs(key, member); - protobufTransaction.addCommands(buildCommand(SIsMember, commandArgs)); + public T sismember(@NonNull ArgType key, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SIsMember, newArgsBuilder().add(key).add(member))); return getThis(); } @@ -890,7 +1421,9 @@ public T sismember(@NonNull String key, @NonNull String member) { * Removes specified members from the set stored at key. Specified members that are * not a member of this set are ignored. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key from which members will be removed. * @param members A list of members to remove from the set stored at key. * @return Command Response - The number of members that were removed from the set, excluding @@ -898,52 +1431,76 @@ public T sismember(@NonNull String key, @NonNull String member) { * @remarks If key does not exist, it is treated as an empty set and this command * returns 0. */ - public T srem(@NonNull String key, @NonNull String[] members) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); - protobufTransaction.addCommands(buildCommand(SRem, commandArgs)); + public T srem(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SRem, newArgsBuilder().add(key).add(members))); return getThis(); } /** * Retrieves all the members of the set value stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key from which to retrieve the set members. * @return Command Response - A Set of all members of the set. * @remarks If key does not exist an empty set will be returned. */ - public T smembers(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(SMembers, commandArgs)); + public T smembers(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SMembers, newArgsBuilder().add(key))); return getThis(); } /** * Retrieves the set cardinality (number of elements) of the set stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key from which to retrieve the number of set members. * @return Command Response - The cardinality (number of elements) of the set, or 0 if the key * does not exist. */ - public T scard(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(SCard, commandArgs)); + public T scard(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SCard, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Computes the difference between the first set and all the successive sets in keys. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys The keys of the sets to diff. + * @return Command Response - A Set of elements representing the difference between + * the sets.
+ * If the a key does not exist, it is treated as an empty set. + */ + public T sdiff(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(SDiff, newArgsBuilder().add(keys))); return getThis(); } /** * Checks whether each member is contained in the members of the set stored at key. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key of the set to check. * @param members A list of members to check for existence in the set. * @return Command Response - An array of Boolean values, each * indicating if the respective member exists in the set. */ - public T smismember(@NonNull String key, @NonNull String[] members) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); - protobufTransaction.addCommands(buildCommand(SMIsMember, commandArgs)); + public T smismember(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SMIsMember, newArgsBuilder().add(key).add(members))); return getThis(); } @@ -951,14 +1508,17 @@ public T smismember(@NonNull String key, @NonNull String[] members) { * Stores the difference between the first set and all the successive sets in keys * into a new set at destination. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param destination The key of the destination set. * @param keys The keys of the sets to diff. * @return Command Response - The number of elements in the resulting set. */ - public T sdiffstore(@NonNull String destination, @NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(keys, destination)); - protobufTransaction.addCommands(buildCommand(SDiffStore, commandArgs)); + public T sdiffstore(@NonNull ArgType destination, @NonNull ArgType[] keys) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand(SDiffStore, newArgsBuilder().add(destination).add(keys))); return getThis(); } @@ -967,31 +1527,37 @@ public T sdiffstore(@NonNull String destination, @NonNull String[] keys) { *
, removing it from the source set. Creates a new destination set if needed. The * operation is atomic. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param source The key of the set to remove the element from. * @param destination The key of the set to add the element to. * @param member The set element to move. * @return Command response - true on success, or false if the - * source set does not exist or the element is not a member of the source set. + * source set does not exist or the element is not a member of the source set. */ - public T smove(@NonNull String source, @NonNull String destination, @NonNull String member) { - ArgsArray commandArgs = buildArgs(source, destination, member); - protobufTransaction.addCommands(buildCommand(SMove, commandArgs)); + public T smove( + @NonNull ArgType source, @NonNull ArgType destination, @NonNull ArgType member) { + checkTypeOrThrow(source); + protobufTransaction.addCommands( + buildCommand(SMove, newArgsBuilder().add(source).add(destination).add(member))); return getThis(); } /** * Gets the intersection of all the given sets. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param keys The keys of the sets. * @return Command Response - A Set of members which are present in all given sets. *
* Missing or empty input sets cause an empty response. */ - public T sinter(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(keys); - protobufTransaction.addCommands(buildCommand(SInter, commandArgs)); + public T sinter(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(SInter, newArgsBuilder().add(keys))); return getThis(); } @@ -999,14 +1565,57 @@ public T sinter(@NonNull String[] keys) { * Stores the members of the intersection of all given sets specified by keys into a * new set at destination. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param destination The key of the destination set. * @param keys The keys from which to retrieve the set members. * @return Command Response - The number of elements in the resulting set. */ - public T sinterstore(@NonNull String destination, @NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(keys, destination)); - protobufTransaction.addCommands(buildCommand(SInterStore, commandArgs)); + public T sinterstore(@NonNull ArgType destination, @NonNull ArgType[] keys) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand(SInterStore, newArgsBuilder().add(destination).add(keys))); + return getThis(); + } + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @return Command Response - The cardinality of the intersection result. If one or more sets do + * not exist, 0 is returned. + */ + public T sintercard(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(SInterCard, newArgsBuilder().add(keys.length).add(keys))); + return getThis(); + } + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys The keys of the sets. + * @param limit The limit for the intersection cardinality value. + * @return Command Response - The cardinality of the intersection result. If one or more sets do + * not exist, 0 is returned. If the intersection cardinality reaches limit + * partway through the computation, returns limit as the cardinality. + */ + public T sintercard(@NonNull ArgType[] keys, long limit) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + SInterCard, + newArgsBuilder().add(keys.length).add(keys).add(SET_LIMIT_VALKEY_API).add(limit))); return getThis(); } @@ -1014,58 +1623,67 @@ public T sinterstore(@NonNull String destination, @NonNull String[] keys) { * Stores the members of the union of all given sets specified by keys into a new set * at destination. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param destination The key of the destination set. * @param keys The keys from which to retrieve the set members. * @return Command Response - The number of elements in the resulting set. */ - public T sunionstore(@NonNull String destination, @NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(keys, destination)); - protobufTransaction.addCommands(buildCommand(SUnionStore, commandArgs)); + public T sunionstore(@NonNull ArgType destination, @NonNull ArgType[] keys) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand(SUnionStore, newArgsBuilder().add(destination).add(keys))); return getThis(); } /** - * Reads the configuration parameters of a running Redis server. + * Reads the configuration parameters of the running server. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param parameters An array of configuration parameter names to retrieve values * for. * @return Command response - A map of values corresponding to the configuration * parameters. */ - public T configGet(@NonNull String[] parameters) { - ArgsArray commandArgs = buildArgs(parameters); - protobufTransaction.addCommands(buildCommand(ConfigGet, commandArgs)); + public T configGet(@NonNull ArgType[] parameters) { + checkTypeOrThrow(parameters); + protobufTransaction.addCommands(buildCommand(ConfigGet, newArgsBuilder().add(parameters))); return getThis(); } /** * Sets configuration parameters to the specified values. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param parameters A map consisting of configuration parameters and their * respective values to set. * @return Command response - OK if all configurations have been successfully set. * Otherwise, the transaction fails with an error. */ - public T configSet(@NonNull Map parameters) { - ArgsArray commandArgs = buildArgs(convertMapToKeyValueStringArray(parameters)); - protobufTransaction.addCommands(buildCommand(ConfigSet, commandArgs)); + public T configSet(@NonNull Map parameters) { + protobufTransaction.addCommands( + buildCommand(ConfigSet, newArgsBuilder().add(flattenMapToGlideStringArray(parameters)))); return getThis(); } /** * Returns the number of keys in keys that exist in the database. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param keys The keys list to check. * @return Command Response - The number of keys that exist. If the same existing key is mentioned * in keys multiple times, it will be counted multiple times. */ - public T exists(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(keys); - protobufTransaction.addCommands(buildCommand(Exists, commandArgs)); + public T exists(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(Exists, newArgsBuilder().add(keys))); return getThis(); } @@ -1073,15 +1691,17 @@ public T exists(@NonNull String[] keys) { * Unlinks (deletes) multiple keys from the database. A key is ignored if it does not * exist. This command, similar to DEL, removes specified keys and ignores non-existent ones. * However, this command does not block the server, while DEL does. + * href="https://valkey.io/commands/del/">DEL does. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param keys The list of keys to unlink. * @return Command Response - The number of keys that were unlinked. */ - public T unlink(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(keys); - protobufTransaction.addCommands(buildCommand(Unlink, commandArgs)); + public T unlink(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(Unlink, newArgsBuilder().add(keys))); return getThis(); } @@ -1095,15 +1715,17 @@ public T unlink(@NonNull String[] keys) { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param seconds The timeout in seconds. * @return Command response - true if the timeout was set. false if the * timeout was not set. e.g. key doesn't exist. */ - public T expire(@NonNull String key, long seconds) { - ArgsArray commandArgs = buildArgs(key, Long.toString(seconds)); - protobufTransaction.addCommands(buildCommand(Expire, commandArgs)); + public T expire(@NonNull ArgType key, long seconds) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Expire, newArgsBuilder().add(key).add(seconds))); return getThis(); } @@ -1117,7 +1739,9 @@ public T expire(@NonNull String key, long seconds) { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param seconds The timeout in seconds. * @param expireOptions The expire options. @@ -1125,12 +1749,11 @@ public T expire(@NonNull String key, long seconds) { * timeout was not set. e.g. key doesn't exist, or operation skipped due to the * provided arguments. */ - public T expire(@NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { - ArgsArray commandArgs = - buildArgs( - ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs())); - - protobufTransaction.addCommands(buildCommand(Expire, commandArgs)); + public T expire( + @NonNull ArgType key, long seconds, @NonNull ExpireOptions expireOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Expire, newArgsBuilder().add(key).add(seconds).add(expireOptions.toArgs()))); return getThis(); } @@ -1144,15 +1767,18 @@ public T expire(@NonNull String key, long seconds, @NonNull ExpireOptions expire * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixSeconds The timeout in an absolute Unix timestamp. * @return Command response - true if the timeout was set. false if the * timeout was not set. e.g. key doesn't exist. */ - public T expireAt(@NonNull String key, long unixSeconds) { - ArgsArray commandArgs = buildArgs(key, Long.toString(unixSeconds)); - protobufTransaction.addCommands(buildCommand(ExpireAt, commandArgs)); + public T expireAt(@NonNull ArgType key, long unixSeconds) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ExpireAt, newArgsBuilder().add(key).add(unixSeconds))); return getThis(); } @@ -1166,7 +1792,9 @@ public T expireAt(@NonNull String key, long unixSeconds) { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixSeconds The timeout in an absolute Unix timestamp. * @param expireOptions The expire options. @@ -1174,41 +1802,43 @@ public T expireAt(@NonNull String key, long unixSeconds) { * timeout was not set. e.g. key doesn't exist, or operation skipped due to the * provided arguments. */ - public T expireAt(@NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { - ArgsArray commandArgs = - buildArgs( - ArrayUtils.addAll( - new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs())); - - protobufTransaction.addCommands(buildCommand(ExpireAt, commandArgs)); + public T expireAt( + @NonNull ArgType key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ExpireAt, newArgsBuilder().add(key).add(unixSeconds).add(expireOptions.toArgs()))); return getThis(); } /** * Sets a timeout on key in milliseconds. After the timeout has expired, the - * key will automatically be deleted.
- * If key already has an existing - * expire set, the time to live is updated to the new value.
+ * key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
* If milliseconds is a non-positive number, the key will be deleted * rather than expired.
* The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param milliseconds The timeout in milliseconds. * @return Command response - true if the timeout was set. false if the * timeout was not set. e.g. key doesn't exist. */ - public T pexpire(@NonNull String key, long milliseconds) { - ArgsArray commandArgs = buildArgs(key, Long.toString(milliseconds)); - protobufTransaction.addCommands(buildCommand(PExpire, commandArgs)); + public T pexpire(@NonNull ArgType key, long milliseconds) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(PExpire, newArgsBuilder().add(key).add(milliseconds))); return getThis(); } /** * Sets a timeout on key in milliseconds. After the timeout has expired, the - * key will automatically be deleted.
+ * key will automatically be deleted.
* If key already has an existing expire set, the time to live is updated to the new * value.
* If milliseconds is a non-positive number, the key will be deleted @@ -1216,7 +1846,9 @@ public T pexpire(@NonNull String key, long milliseconds) { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param milliseconds The timeout in milliseconds. * @param expireOptions The expire options. @@ -1224,13 +1856,12 @@ public T pexpire(@NonNull String key, long milliseconds) { * timeout was not set. e.g. key doesn't exist, or operation skipped due to the * provided arguments. */ - public T pexpire(@NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { - ArgsArray commandArgs = - buildArgs( - ArrayUtils.addAll( - new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs())); - - protobufTransaction.addCommands(buildCommand(PExpire, commandArgs)); + public T pexpire( + @NonNull ArgType key, long milliseconds, @NonNull ExpireOptions expireOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + PExpire, newArgsBuilder().add(key).add(milliseconds).add(expireOptions.toArgs()))); return getThis(); } @@ -1244,16 +1875,18 @@ public T pexpire(@NonNull String key, long milliseconds, @NonNull ExpireOptions * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixMilliseconds The timeout in an absolute Unix timestamp. * @return Command response - true if the timeout was set. false if the * timeout was not set. e.g. key doesn't exist. */ - public T pexpireAt(@NonNull String key, long unixMilliseconds) { - ArgsArray commandArgs = buildArgs(key, Long.toString(unixMilliseconds)); - - protobufTransaction.addCommands(buildCommand(PExpireAt, commandArgs)); + public T pexpireAt(@NonNull ArgType key, long unixMilliseconds) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(PExpireAt, newArgsBuilder().add(key).add(unixMilliseconds))); return getThis(); } @@ -1267,7 +1900,9 @@ public T pexpireAt(@NonNull String key, long unixMilliseconds) { * The timeout will only be cleared by commands that delete or overwrite the contents of key * . * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to set timeout on it. * @param unixMilliseconds The timeout in an absolute Unix timestamp. * @param expireOptions The expiration option. @@ -1275,36 +1910,76 @@ public T pexpireAt(@NonNull String key, long unixMilliseconds) { * timeout was not set. e.g. key doesn't exist, or operation skipped due to the * provided arguments. */ - public T pexpireAt( - @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { - ArgsArray commandArgs = - buildArgs( - ArrayUtils.addAll( - new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs())); - - protobufTransaction.addCommands(buildCommand(PExpireAt, commandArgs)); + public T pexpireAt( + @NonNull ArgType key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + PExpireAt, + newArgsBuilder().add(key).add(unixMilliseconds).add(expireOptions.toArgs()))); return getThis(); } /** * Returns the remaining time to live of key that has a timeout. * - * @see redis.io for details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. * @param key The key to return its timeout. * @return Command response - TTL in seconds, -2 if key does not exist, * or -1 if key exists but has no associated expire. */ - public T ttl(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); + public T ttl(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(TTL, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key + * will expire, in seconds.
+ * To get the expiration with millisecond precision, use {@link #pexpiretime(ArgType)}. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to determine the expiration value of. + * @return Command response - The expiration Unix timestamp in seconds, -2 if + * key does not exist, or -1 if key exists but has no + * associated expiration. + */ + public T expiretime(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ExpireTime, newArgsBuilder().add(key))); + return getThis(); + } - protobufTransaction.addCommands(buildCommand(TTL, commandArgs)); + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given key + * will expire, in milliseconds. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to determine the expiration value of. + * @return Command response - The expiration Unix timestamp in milliseconds, -2 if + * key + * does not exist, or -1 if key exists but has no associated + * expiration. + */ + public T pexpiretime(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(PExpireTime, newArgsBuilder().add(key))); return getThis(); } /** * Gets the current connection id. * - * @see redis.io for details. + * @see valkey.io for details. * @return Command response - The id of the client. */ public T clientId() { @@ -1315,7 +1990,7 @@ public T clientId() { /** * Gets the name of the current connection. * - * @see redis.io for details. + * @see valkey.io for details. * @return Command response - The name of the client connection as a string if a name is set, or * null if no name is assigned. */ @@ -1327,9 +2002,9 @@ public T clientGetName() { /** * Rewrites the configuration file with the current configuration. * - * @see redis.io for details. - * @return OK is returned when the configuration was rewritten properly. Otherwise, - * the transaction fails with an error. + * @see valkey.io for details. + * @return Command Response - OK is returned when the configuration was rewritten + * properly. Otherwise, the transaction fails with an error. */ public T configRewrite() { protobufTransaction.addCommands(buildCommand(ConfigRewrite)); @@ -1337,12 +2012,13 @@ public T configRewrite() { } /** - * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands. * - * @see redis.io for details. - * @return OK to confirm that the statistics were successfully reset. + * @see valkey.io for details. + * @return Command Response - OK to confirm that the statistics were successfully + * reset. */ public T configResetStat() { protobufTransaction.addCommands(buildCommand(ConfigResetStat)); @@ -1353,29 +2029,30 @@ public T configResetStat() { * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. - * @param options The Zadd options. + * @param options The ZAdd options. * @param changed Modify the return value from the number of new elements added, to the total * number of elements changed. * @return Command Response - The number of elements added to the sorted set.
* If changed is set, returns the number of elements updated in the sorted set. */ - public T zadd( - @NonNull String key, - @NonNull Map membersScoresMap, - @NonNull ZaddOptions options, + public T zadd( + @NonNull ArgType key, + @NonNull Map membersScoresMap, + @NonNull ZAddOptions options, boolean changed) { - String[] changedArg = changed ? new String[] {"CH"} : new String[] {}; - String[] membersScores = convertMapToValueKeyStringArray(membersScoresMap); - - String[] arguments = - concatenateArrays(new String[] {key}, options.toArgs(), changedArg, membersScores); - - ArgsArray commandArgs = buildArgs(arguments); - - protobufTransaction.addCommands(buildCommand(Zadd, commandArgs)); + checkTypeOrThrow(key); + ArgsBuilder args = new ArgsBuilder(); + args.add(key).add(options.toArgs()); + if (changed) { + args.add("CH"); + } + args.add(flattenMapToGlideStringArrayValueFirst(membersScoresMap)); + protobufTransaction.addCommands(buildCommand(ZAdd, args)); return getThis(); } @@ -1383,16 +2060,18 @@ public T zadd( * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. - * @param options The Zadd options. + * @param options The ZAdd options. * @return Command Response - The number of elements added to the sorted set. */ - public T zadd( - @NonNull String key, - @NonNull Map membersScoresMap, - @NonNull ZaddOptions options) { + public T zadd( + @NonNull ArgType key, + @NonNull Map membersScoresMap, + @NonNull ZAddOptions options) { return zadd(key, membersScoresMap, options, false); } @@ -1400,7 +2079,9 @@ public T zadd( * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. * @param changed Modify the return value from the number of new elements added, to the total @@ -1408,77 +2089,91 @@ public T zadd( * @return Command Response - The number of elements added to the sorted set.
* If changed is set, returns the number of elements updated in the sorted set. */ - public T zadd( - @NonNull String key, @NonNull Map membersScoresMap, boolean changed) { - return zadd(key, membersScoresMap, ZaddOptions.builder().build(), changed); + public T zadd( + @NonNull ArgType key, @NonNull Map membersScoresMap, boolean changed) { + return zadd(key, membersScoresMap, ZAddOptions.builder().build(), changed); } /** * Adds members with their scores to the sorted set stored at key.
* If a member is already a part of the sorted set, its score is updated. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param membersScoresMap A Map of members to their corresponding scores. * @return Command Response - The number of elements added to the sorted set. */ - public T zadd(@NonNull String key, @NonNull Map membersScoresMap) { - return zadd(key, membersScoresMap, ZaddOptions.builder().build(), false); + public T zadd(@NonNull ArgType key, @NonNull Map membersScoresMap) { + return zadd(key, membersScoresMap, ZAddOptions.builder().build(), false); } /** * Increments the score of member in the sorted set stored at key by increment * .
- * If member does not exist in the sorted set, it is added with - * increment as its score (as if its previous score was 0.0).
+ * If member does not exist in the sorted set, it is added with increment + * as its score (as if its previous score was 0.0).
* If key does not exist, a new sorted set with the specified member as its sole - * member is created. + * member is created.
+ * zaddIncr with empty option acts as {@link #zincrby(ArgType, double, ArgType)}. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member A member in the sorted set to increment. * @param increment The score to increment the member. - * @param options The Zadd options. + * @param options The ZAdd options. * @return Command Response - The score of the member.
* If there was a conflict with the options, the operation aborts and null is - * returned.
+ * returned. */ - public T zaddIncr( - @NonNull String key, @NonNull String member, double increment, @NonNull ZaddOptions options) { - ArgsArray commandArgs = - buildArgs( - concatenateArrays( - new String[] {key}, - options.toArgs(), - new String[] {"INCR", Double.toString(increment), member})); - - protobufTransaction.addCommands(buildCommand(Zadd, commandArgs)); + public T zaddIncr( + @NonNull ArgType key, + @NonNull ArgType member, + double increment, + @NonNull ZAddOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZAdd, + newArgsBuilder() + .add(key) + .add(options.toArgs()) + .add("INCR") + .add(increment) + .add(member))); return getThis(); } /** * Increments the score of member in the sorted set stored at key by increment * .
- * If member does not exist in the sorted set, it is added with - * increment as its score (as if its previous score was 0.0).
+ * If member does not exist in the sorted set, it is added with increment + * as its score (as if its previous score was 0.0).
* If key does not exist, a new sorted set with the specified member as its sole * member is created. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member A member in the sorted set to increment. * @param increment The score to increment the member. * @return Command Response - The score of the member. */ - public T zaddIncr(@NonNull String key, @NonNull String member, double increment) { - return zaddIncr(key, member, increment, ZaddOptions.builder().build()); + public T zaddIncr(@NonNull ArgType key, @NonNull ArgType member, double increment) { + return zaddIncr(key, member, increment, ZAddOptions.builder().build()); } /** * Removes the specified members from the sorted set stored at key.
* Specified members that are not a member of this set are ignored. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param members An array of members to remove from the sorted set. * @return Command Response - The number of members that were removed from the sorted set, not @@ -1486,24 +2181,26 @@ public T zaddIncr(@NonNull String key, @NonNull String member, double increment) * If key does not exist, it is treated as an empty sorted set, and this command * returns 0. */ - public T zrem(@NonNull String key, @NonNull String[] members) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); - protobufTransaction.addCommands(buildCommand(Zrem, commandArgs)); + public T zrem(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZRem, newArgsBuilder().add(key).add(members))); return getThis(); } /** * Returns the cardinality (number of elements) of the sorted set stored at key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @return Command Response - The number of elements in the sorted set.
* If key does not exist, it is treated as an empty sorted set, and this command * return 0. */ - public T zcard(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Zcard, commandArgs)); + public T zcard(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZCard, newArgsBuilder().add(key))); return getThis(); } @@ -1511,7 +2208,9 @@ public T zcard(@NonNull String key) { * Removes and returns up to count members with the lowest scores from the sorted set * stored at the specified key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param count Specifies the quantity of members to pop.
* If count is higher than the sorted set's cardinality, returns all members and @@ -1521,9 +2220,9 @@ public T zcard(@NonNull String key) { * If key doesn't exist, it will be treated as an empty sorted set and the * command returns an empty Map. */ - public T zpopmin(@NonNull String key, long count) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count)); - protobufTransaction.addCommands(buildCommand(ZPopMin, commandArgs)); + public T zpopmin(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZPopMin, newArgsBuilder().add(key).add(count))); return getThis(); } @@ -1531,15 +2230,129 @@ public T zpopmin(@NonNull String key, long count) { * Removes and returns the member with the lowest score from the sorted set stored at the * specified key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @return Command Response - A map containing the removed member and its corresponding score.
* If key doesn't exist, it will be treated as an empty sorted set and the * command returns an empty Map. */ - public T zpopmin(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(ZPopMin, commandArgs)); + public T zpopmin(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZPopMin, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns a random element from the sorted set stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @return Command Response - A String representing a random element from the sorted + * set.
+ * If the sorted set does not exist or is empty, the response will be null. + */ + public T zrandmember(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZRandMember, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Retrieves random elements from the sorted set stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return Command Response - An array of elements from the sorted set.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + */ + public T zrandmemberWithCount(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ZRandMember, newArgsBuilder().add(key).add(count))); + return getThis(); + } + + /** + * Retrieves random elements along with their scores from the sorted set stored at key + * . + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows duplicates.
+ * @return Command Response - An array of [element, + * score] arrays + * , where element is a String and score is a Double.
+ * If the sorted set does not exist or is empty, the response will be an empty array + * . + */ + public T zrandmemberWithCountWithScores(ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZRandMember, newArgsBuilder().add(key).add(count).add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Increments the score of member in the sorted set stored at key by + * increment.
+ * If member does not exist in the sorted set, it is added with increment + * as its score. If key does not exist, a new sorted set with the specified + * member as its sole member is created. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param increment The score increment. + * @param member A member of the sorted set. + * @return Command Response - The new score of member. + */ + public T zincrby(@NonNull ArgType key, double increment, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ZIncrBy, newArgsBuilder().add(key).add(increment).add(member))); + return getThis(); + } + + /** + * Blocks the connection until it removes and returns a member with the lowest score from the + * sorted sets stored at the specified keys. The sorted sets are checked in the order + * they are provided.
+ * BZPOPMIN is the blocking variant of {@link #zpopmin(ArgType)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @apiNote BZPOPMIN is a client blocking command, see Blocking + * Commands for more details and best practices. + * @param keys The keys of the sorted sets. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - An array containing the key where the member was popped + * out, the member itself, and the member score.
+ * If no member could be popped and the timeout expired, returns null + * . + */ + public T bzpopmin(@NonNull ArgType[] keys, double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(BZPopMin, newArgsBuilder().add(keys).add(timeout))); return getThis(); } @@ -1547,7 +2360,9 @@ public T zpopmin(@NonNull String key) { * Removes and returns up to count members with the highest scores from the sorted * set stored at the specified key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param count Specifies the quantity of members to pop.
* If count is higher than the sorted set's cardinality, returns all members and @@ -1557,9 +2372,9 @@ public T zpopmin(@NonNull String key) { * If key doesn't exist, it will be treated as an empty sorted set and the * command returns an empty Map. */ - public T zpopmax(@NonNull String key, long count) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count)); - protobufTransaction.addCommands(buildCommand(ZPopMax, commandArgs)); + public T zpopmax(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZPopMax, newArgsBuilder().add(key).add(count))); return getThis(); } @@ -1567,67 +2382,146 @@ public T zpopmax(@NonNull String key, long count) { * Removes and returns the member with the highest score from the sorted set stored at the * specified key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @return Command Response - A map containing the removed member and its corresponding score.
* If key doesn't exist, it will be treated as an empty sorted set and the * command returns an empty Map. */ - public T zpopmax(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(ZPopMax, commandArgs)); + public T zpopmax(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZPopMax, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Blocks the connection until it removes and returns a member with the highest score from the + * sorted sets stored at the specified keys. The sorted sets are checked in the order + * they are provided.
+ * BZPOPMAX is the blocking variant of {@link #zpopmax(ArgType)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @apiNote BZPOPMAX is a client blocking command, see Blocking + * Commands for more details and best practices. + * @param keys The keys of the sorted sets. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - An array containing the key where the member was popped + * out, the member itself, and the member score.
+ * If no member could be popped and the timeout expired, returns null + * . + */ + public T bzpopmax(@NonNull ArgType[] keys, double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(BZPopMax, newArgsBuilder().add(keys).add(timeout))); return getThis(); } /** * Returns the score of member in the sorted set stored at key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member The member whose score is to be retrieved. * @return Command Response - The score of the member.
* If member does not exist in the sorted set, null is returned.
* If key does not exist, null is returned. */ - public T zscore(@NonNull String key, @NonNull String member) { - ArgsArray commandArgs = buildArgs(key, member); - protobufTransaction.addCommands(buildCommand(ZScore, commandArgs)); + public T zscore(@NonNull ArgType key, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZScore, newArgsBuilder().add(key).add(member))); return getThis(); } /** * Returns the rank of member in the sorted set stored at key, with - * scores ordered from low to high.
+ * scores ordered from low to high, starting from 0.
* To get the rank of member with its score, see {@link #zrankWithScore}. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return Command Response - The rank of member in the sorted set.
+ * If key doesn't exist, or if member is not present in the set, + * null will be returned. + */ + public T zrank(@NonNull ArgType key, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZRank, newArgsBuilder().add(key).add(member))); + return getThis(); + } + + /** + * Returns the rank of member in the sorted set stored at key with its + * score, where scores are ordered from the lowest to highest, starting from 0. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return Command Response - An array containing the rank (as Long) and + * score (as Double) of member in the sorted set.
+ * If key doesn't exist, or if member is not present in the set, + * null will be returned. + */ + public T zrankWithScore(@NonNull ArgType key, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ZRank, newArgsBuilder().add(key).add(member).add(WITH_SCORE_VALKEY_API))); + return getThis(); + } + + /** + * Returns the rank of member in the sorted set stored at key, where + * scores are ordered from the highest to lowest, starting from 0.
+ * To get the rank of member with its score, see {@link #zrevrankWithScore}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member The member whose rank is to be retrieved. - * @return The rank of member in the sorted set.
+ * @return Command Response - The rank of member in the sorted set, where ranks are + * ordered from high to low based on scores.
* If key doesn't exist, or if member is not present in the set, * null will be returned. */ - public T zrank(@NonNull String key, @NonNull String member) { - ArgsArray commandArgs = buildArgs(key, member); - protobufTransaction.addCommands(buildCommand(Zrank, commandArgs)); + public T zrevrank(@NonNull ArgType key, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZRevRank, newArgsBuilder().add(key).add(member))); return getThis(); } /** * Returns the rank of member in the sorted set stored at key with its - * score, where scores are ordered from the lowest to highest. + * score, where scores are ordered from the highest to lowest, starting from 0. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param member The member whose rank is to be retrieved. - * @return An array containing the rank (as Long) and score (as Double) - * of member in the sorted set.
+ * @return Command Response - An array containing the rank (as Long) and + * score (as Double) of member in the sorted set, where ranks are + * ordered from high to low based on scores.
* If key doesn't exist, or if member is not present in the set, * null will be returned. */ - public T zrankWithScore(@NonNull String key, @NonNull String member) { - ArgsArray commandArgs = buildArgs(key, member, WITH_SCORE_REDIS_API); - protobufTransaction.addCommands(buildCommand(Zrank, commandArgs)); + public T zrevrankWithScore(@NonNull ArgType key, @NonNull ArgType member) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ZRevRank, newArgsBuilder().add(key).add(member).add(WITH_SCORE_VALKEY_API))); return getThis(); } @@ -1635,16 +2529,18 @@ public T zrankWithScore(@NonNull String key, @NonNull String member) { * Returns the scores associated with the specified members in the sorted set stored * at key. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param members An array of members in the sorted set. * @return Command Response - An Array of scores of the members.
* If a member does not exist, the corresponding value in the Array * will be null. */ - public T zmscore(@NonNull String key, @NonNull String[] members) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); - protobufTransaction.addCommands(buildCommand(ZMScore, commandArgs)); + public T zmscore(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZMScore, newArgsBuilder().add(key).add(members))); return getThis(); } @@ -1652,34 +2548,41 @@ public T zmscore(@NonNull String key, @NonNull String[] members) { * Returns the difference between the first sorted set and all the successive sorted sets.
* To get the elements with their scores, see {@link #zdiffWithScores}. * - * @see redis.io for more details. + * @since Valkey 6.2 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param keys The keys of the sorted sets. * @return Command Response - An array of elements representing the difference * between the sorted sets.
* If the first key does not exist, it is treated as an empty sorted set, and the * command returns an empty array. */ - public T zdiff(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(keys, Long.toString(keys.length))); - protobufTransaction.addCommands(buildCommand(ZDiff, commandArgs)); + public T zdiff(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(ZDiff, newArgsBuilder().add(keys.length).add(keys))); return getThis(); } /** * Returns the difference between the first sorted set and all the successive sorted sets. * - * @see redis.io for more details. + * @since Valkey 6.2 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param keys The keys of the sorted sets. * @return Command Response - A Map of elements and their scores representing the * difference between the sorted sets.
* If the first key does not exist, it is treated as an empty sorted set, and the * command returns an empty Map. */ - public T zdiffWithScores(@NonNull String[] keys) { - String[] arguments = ArrayUtils.addFirst(keys, Long.toString(keys.length)); - arguments = ArrayUtils.add(arguments, WITH_SCORES_REDIS_API); - ArgsArray commandArgs = buildArgs(arguments); - protobufTransaction.addCommands(buildCommand(ZDiff, commandArgs)); + public T zdiffWithScores(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + ZDiff, newArgsBuilder().add(keys.length).add(keys).add(WITH_SCORES_VALKEY_API))); return getThis(); } @@ -1688,16 +2591,19 @@ public T zdiffWithScores(@NonNull String[] keys) { * keys and stores the difference as a sorted set to destination, * overwriting it if it already exists. Non-existent keys are treated as empty sets. * - * @see redis.io for more details. + * @since Valkey 6.2 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param destination The key for the resulting sorted set. * @param keys The keys of the sorted sets to compare. * @return Command Response - The number of members in the resulting sorted set stored at - * destination. + * destination. */ - public T zdiffstore(@NonNull String destination, @NonNull String[] keys) { - ArgsArray commandArgs = - buildArgs(ArrayUtils.addAll(new String[] {destination, Long.toString(keys.length)}, keys)); - protobufTransaction.addCommands(buildCommand(ZDiffStore, commandArgs)); + public T zdiffstore(@NonNull ArgType destination, @NonNull ArgType[] keys) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand(ZDiffStore, newArgsBuilder().add(destination).add(keys.length).add(keys))); return getThis(); } @@ -1705,7 +2611,9 @@ public T zdiffstore(@NonNull String destination, @NonNull String[] keys) { * Returns the number of members in the sorted set stored at key with scores between * minScore and maxScore. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minScore The minimum score to count from. Can be an implementation of {@link * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} @@ -1718,9 +2626,12 @@ public T zdiffstore(@NonNull String destination, @NonNull String[] keys) { * returns 0.
* If maxScore < minScore, 0 is returned. */ - public T zcount(@NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { - ArgsArray commandArgs = buildArgs(key, minScore.toArgs(), maxScore.toArgs()); - protobufTransaction.addCommands(buildCommand(Zcount, commandArgs)); + public T zcount( + @NonNull ArgType key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZCount, newArgsBuilder().add(key).add(minScore.toArgs()).add(maxScore.toArgs()))); return getThis(); } @@ -1730,7 +2641,9 @@ public T zcount(@NonNull String key, @NonNull ScoreRange minScore, @NonNull Scor * indexes with 0 being the element with the lowest score. These indexes can be * negative numbers, where they indicate offsets starting at the element with the highest score. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param start The starting point of the range. * @param end The end of the range. @@ -1741,18 +2654,21 @@ public T zcount(@NonNull String key, @NonNull ScoreRange minScore, @NonNull Scor * actual end of the sorted set.
* If key does not exist 0 will be returned. */ - public T zremrangebyrank(@NonNull String key, long start, long end) { - ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); - protobufTransaction.addCommands(buildCommand(ZRemRangeByRank, commandArgs)); + public T zremrangebyrank(@NonNull ArgType key, long start, long end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ZRemRangeByRank, newArgsBuilder().add(key).add(start).add(end))); return getThis(); } /** * Stores a specified range of elements from the sorted set at source, into a new * sorted set at destination. If destination doesn't exist, a new sorted - * set is created; if it exists, it's overwritten.
+ * set is created; if it exists, it's overwritten. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param destination The key for the destination sorted set. * @param source The key of the source sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
@@ -1766,23 +2682,30 @@ public T zremrangebyrank(@NonNull String key, long start, long end) { * element with the highest score. * @return Command Response - The number of elements in the resulting sorted set. */ - public T zrangestore( - @NonNull String destination, - @NonNull String source, + public T zrangestore( + @NonNull ArgType destination, + @NonNull ArgType source, @NonNull RangeQuery rangeQuery, boolean reverse) { - ArgsArray commandArgs = - buildArgs(RangeOptions.createZRangeStoreArgs(destination, source, rangeQuery, reverse)); - protobufTransaction.addCommands(buildCommand(ZRangeStore, commandArgs)); + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand( + ZRangeStore, + newArgsBuilder() + .add(destination) + .add(source) + .add(RangeOptions.createZRangeBaseArgs(rangeQuery, reverse, false)))); return getThis(); } /** * Stores a specified range of elements from the sorted set at source, into a new * sorted set at destination. If destination doesn't exist, a new sorted - * set is created; if it exists, it's overwritten.
+ * set is created; if it exists, it's overwritten. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param destination The key for the destination sorted set. * @param source The key of the source sorted set. * @param rangeQuery The range query object representing the type of range query to perform.
@@ -1794,8 +2717,9 @@ public T zrangestore( * * @return Command Response - The number of elements in the resulting sorted set. */ - public T zrangestore( - @NonNull String destination, @NonNull String source, @NonNull RangeQuery rangeQuery) { + public T zrangestore( + @NonNull ArgType destination, @NonNull ArgType source, @NonNull RangeQuery rangeQuery) { + checkTypeOrThrow(destination); return getThis().zrangestore(destination, source, rangeQuery, false); } @@ -1803,7 +2727,9 @@ public T zrangestore( * Removes all elements in the sorted set stored at key with a lexicographical order * between minLex and maxLex. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minLex The minimum bound of the lexicographical range. Can be an implementation of * {@link InfLexBound} representing positive/negative infinity, or {@link LexBoundary} @@ -1816,17 +2742,22 @@ public T zrangestore( * returns 0.
* If minLex is greater than maxLex, 0 is returned. */ - public T zremrangebylex(@NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { - ArgsArray commandArgs = buildArgs(key, minLex.toArgs(), maxLex.toArgs()); - protobufTransaction.addCommands(buildCommand(ZRemRangeByLex, commandArgs)); + public T zremrangebylex( + @NonNull ArgType key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZRemRangeByLex, newArgsBuilder().add(key).add(minLex.toArgs()).add(maxLex.toArgs()))); return getThis(); } /** * Removes all elements in the sorted set stored at key with a score between - * minScore and maxScore. + * minScore and maxScore. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minScore The minimum score to remove from. Can be an implementation of {@link * InfScoreBound} representing positive/negative infinity, or {@link ScoreBoundary} @@ -1839,10 +2770,13 @@ public T zremrangebylex(@NonNull String key, @NonNull LexRange minLex, @NonNull * returns 0.
* If minScore is greater than maxScore, 0 is returned. */ - public T zremrangebyscore( - @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { - ArgsArray commandArgs = buildArgs(key, minScore.toArgs(), maxScore.toArgs()); - protobufTransaction.addCommands(buildCommand(ZRemRangeByScore, commandArgs)); + public T zremrangebyscore( + @NonNull ArgType key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZRemRangeByScore, + newArgsBuilder().add(key).add(minScore.toArgs()).add(maxScore.toArgs()))); return getThis(); } @@ -1850,7 +2784,9 @@ public T zremrangebyscore( * Returns the number of members in the sorted set stored at key with scores between * minLex and maxLex. * - * @see redis.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. * @param minLex The minimum lex to count from. Can be an implementation of {@link InfLexBound} * representing positive/negative infinity, or {@link LexBoundary} representing a specific lex @@ -1863,370 +2799,4303 @@ public T zremrangebyscore( * returns 0.
* If maxLex < minLex, 0 is returned. */ - public T zlexcount(@NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { - ArgsArray commandArgs = buildArgs(key, minLex.toArgs(), maxLex.toArgs()); - protobufTransaction.addCommands(buildCommand(ZLexCount, commandArgs)); + public T zlexcount( + @NonNull ArgType key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZLexCount, newArgsBuilder().add(key).add(minLex.toArgs()).add(maxLex.toArgs()))); return getThis(); } /** - * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. * - * @see redis.io for details. - * @param key The key of the stream. - * @param values Field-value pairs to be added to the entry. - * @return Command Response - The id of the added entry. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link WeightAggregateOptions.KeyArray} for keys only. + *
  • Use {@link WeightAggregateOptions.WeightedKeys} for weighted keys with score + * multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. */ - public T xadd(@NonNull String key, @NonNull Map values) { - return xadd(key, values, StreamAddOptions.builder().build()); + public T zunionstore( + @NonNull String destination, + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZUnionStore, + newArgsBuilder() + .add(destination) + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()))); + return getThis(); } /** - * Adds an entry to the specified stream stored at key.
- * If the key doesn't exist, the stream is created. + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. * - * @see redis.io for details. - * @param key The key of the stream. - * @param values Field-value pairs to be added to the entry. - * @param options Stream add options. - * @return Command Response - The id of the added entry, or null if {@link - * StreamAddOptionsBuilder#makeStream(Boolean)} is set to false and no stream - * with the matching key exists. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link WeightAggregateOptions.KeyArrayBinary} for keys only. + *
  • Use {@link WeightAggregateOptions.WeightedKeysBinary} for weighted keys with score + * multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. */ - public T xadd( - @NonNull String key, @NonNull Map values, @NonNull StreamAddOptions options) { - String[] arguments = - ArrayUtils.addAll( - ArrayUtils.addFirst(options.toArgs(), key), convertMapToKeyValueStringArray(values)); - ArgsArray commandArgs = buildArgs(arguments); - protobufTransaction.addCommands(buildCommand(XAdd, commandArgs)); + public T zunionstore( + @NonNull GlideString destination, + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZUnionStore, + newArgsBuilder() + .add(destination) + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()))); return getThis(); } /** - * Returns the remaining time to live of key that has a timeout, in milliseconds. + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. * - * @see redis.io for details. - * @param key The key to return its timeout. - * @return Command Response - TTL in milliseconds. -2 if key does not - * exist, -1 if key exists but has no associated expire. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. */ - public T pttl(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(PTTL, commandArgs)); + public T zunionstore( + @NonNull String destination, @NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZUnionStore, newArgsBuilder().add(destination).add(keysOrWeightedKeys.toArgs()))); return getThis(); } /** - * Removes the existing timeout on key, turning the key from volatile (a - * key with an expire set) to persistent (a key that will never expire - * as no timeout is associated). + * Computes the union of sorted sets given by the specified KeysOrWeightedKeys, and + * stores the result in destination. If destination already exists, it + * is overwritten. Otherwise, a new sorted set will be created. * - * @see redis.io for details. - * @param key The key to remove the existing timeout on. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. + */ + public T zunionstore( + @NonNull GlideString destination, @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZUnionStore, newArgsBuilder().add(destination).add(keysOrWeightedKeys.toArgs()))); + return getThis(); + } + + /** + * Computes the intersection of sorted sets given by the specified keysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link WeightAggregateOptions.KeyArray} for keys only. + *
  • Use {@link WeightAggregateOptions.WeightedKeys} for weighted keys with score + * multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. + */ + public T zinterstore( + @NonNull String destination, + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZInterStore, + newArgsBuilder() + .add(destination) + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()))); + return getThis(); + } + + /** + * Computes the intersection of sorted sets given by the specified keysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link WeightAggregateOptions.KeyArrayBinary} for keys only. + *
  • Use {@link WeightAggregateOptions.WeightedKeysBinary} for weighted keys with score + * multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. + */ + public T zinterstore( + @NonNull GlideString destination, + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, + @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZInterStore, + newArgsBuilder() + .add(destination) + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()))); + return getThis(); + } + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @return Command Response - The cardinality of the intersection of the given sorted sets. + */ + public T zintercard(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(ZInterCard, newArgsBuilder().add(keys.length).add(keys))); + return getThis(); + } + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * If the intersection cardinality reaches limit partway through the computation, the + * algorithm will exit early and yield limit as the cardinality. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @param limit Specifies a maximum number for the intersection cardinality. If limit is set to + * 0 the range will be unlimited. + * @return Command Response - The cardinality of the intersection of the given sorted sets, or the + * limit if reached. + */ + public T zintercard(@NonNull ArgType[] keys, long limit) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + ZInterCard, + newArgsBuilder().add(keys.length).add(keys).add(LIMIT_VALKEY_API).add(limit))); + return getThis(); + } + + /** + * Computes the intersection of sorted sets given by the specified KeysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created.
+ * To perform a zinterstore operation while specifying aggregation settings, use + * {@link #zinterstore(Object, KeysOrWeightedKeys, Aggregate)}. + * + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. + */ + public T zinterstore( + @NonNull String destination, @NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZInterStore, newArgsBuilder().add(destination).add(keysOrWeightedKeys.toArgs()))); + return getThis(); + } + + /** + * Computes the intersection of sorted sets given by the specified KeysOrWeightedKeys + * , and stores the result in destination. If destination already + * exists, it is overwritten. Otherwise, a new sorted set will be created.
+ * To perform a zinterstore operation while specifying aggregation settings, use + * {@link #zinterstore(Object, KeysOrWeightedKeys, Aggregate)}. + * + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link KeysOrWeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The number of elements in the resulting sorted set stored at + * destination. + */ + public T zinterstore( + @NonNull GlideString destination, @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZInterStore, newArgsBuilder().add(destination).add(keysOrWeightedKeys.toArgs()))); + return getThis(); + } + + /** + * Returns the union of members from sorted sets specified by the given keys.
+ * To get the elements with their scores, see {@link #zunionWithScores}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return Command Response - The resulting sorted set from the union. + */ + public T zunion(@NonNull KeyArray keys) { + protobufTransaction.addCommands(buildCommand(ZUnion, newArgsBuilder().add(keys.toArgs()))); + return getThis(); + } + + /** + * Returns the union of members from sorted sets specified by the given keys.
+ * To get the elements with their scores, see {@link #zunionWithScores}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return Command Response - The resulting sorted set from the union. + */ + public T zunion(@NonNull KeyArrayBinary keys) { + protobufTransaction.addCommands(buildCommand(ZUnion, newArgsBuilder().add(keys.toArgs()))); + return getThis(); + } + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The resulting sorted set from the union. + */ + public T zunionWithScores( + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZUnion, + newArgsBuilder() + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The resulting sorted set from the union. + */ + public T zunionWithScores( + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZUnion, + newArgsBuilder() + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys.
+ * To perform a zunion operation while specifying aggregation settings, use {@link + * #zunionWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The resulting sorted set from the union. + */ + public T zunionWithScores(@NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZUnion, newArgsBuilder().add(keysOrWeightedKeys.toArgs()).add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the union of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys.
+ * To perform a zunion operation while specifying aggregation settings, use {@link + * #zunionWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The resulting sorted set from the union. + */ + public T zunionWithScores(@NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZUnion, newArgsBuilder().add(keysOrWeightedKeys.toArgs()).add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the intersection of members from sorted sets specified by the given keys. + *
+ * To get the elements with their scores, see {@link #zinterWithScores}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return Command Response - The resulting sorted set from the intersection. + */ + public T zinter(@NonNull KeyArray keys) { + protobufTransaction.addCommands(buildCommand(ZInter, newArgsBuilder().add(keys.toArgs()))); + return getThis(); + } + + /** + * Returns the intersection of members from sorted sets specified by the given keys. + *
+ * To get the elements with their scores, see {@link #zinterWithScores}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @return Command Response - The resulting sorted set from the intersection. + */ + public T zinter(@NonNull KeyArrayBinary keys) { + protobufTransaction.addCommands(buildCommand(ZInter, newArgsBuilder().add(keys.toArgs()))); + return getThis(); + } + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. To perform a zinter operation while specifying + * aggregation settings, use {@link #zinterWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The resulting sorted set from the intersection. + */ + public T zinterWithScores(@NonNull KeysOrWeightedKeys keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZInter, newArgsBuilder().add(keysOrWeightedKeys.toArgs()).add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. To perform a zinter operation while specifying + * aggregation settings, use {@link #zinterWithScores(KeysOrWeightedKeys, Aggregate)}. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @return Command Response - The resulting sorted set from the intersection. + */ + public T zinterWithScores(@NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys) { + protobufTransaction.addCommands( + buildCommand( + ZInter, newArgsBuilder().add(keysOrWeightedKeys.toArgs()).add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArray} for keys only. + *
  • Use {@link WeightedKeys} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The resulting sorted set from the intersection. + */ + public T zinterWithScores( + @NonNull KeysOrWeightedKeys keysOrWeightedKeys, @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZInter, + newArgsBuilder() + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Returns the intersection of members and their scores from sorted sets specified by the given + * keysOrWeightedKeys. + * + * @since Valkey 6.2 and above. + * @see valkey.io for more details. + * @param keysOrWeightedKeys The keys of the sorted sets with possible formats: + *
    + *
  • Use {@link KeyArrayBinary} for keys only. + *
  • Use {@link WeightedKeysBinary} for weighted keys with score multipliers. + *
+ * + * @param aggregate Specifies the aggregation strategy to apply when combining the scores of + * elements. + * @return Command Response - The resulting sorted set from the intersection. + */ + public T zinterWithScores( + @NonNull KeysOrWeightedKeysBinary keysOrWeightedKeys, @NonNull Aggregate aggregate) { + protobufTransaction.addCommands( + buildCommand( + ZInter, + newArgsBuilder() + .add(keysOrWeightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API))); + return getThis(); + } + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @return Command Response - The id of the added entry. + */ + public T xadd(@NonNull ArgType key, @NonNull Map values) { + return xadd(key, values, StreamAddOptions.builder().build()); + } + + /** + * Adds an entry to the specified stream stored at key.
+ * If the key doesn't exist, the stream is created. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param values Field-value pairs to be added to the entry. + * @param options Stream add options {@link StreamAddOptions}. + * @return Command Response - The id of the added entry, or null if {@link + * StreamAddOptionsBuilder#makeStream(Boolean)} is set to false and no stream + * with the matching key exists. + */ + public T xadd( + @NonNull ArgType key, + @NonNull Map values, + @NonNull StreamAddOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XAdd, + newArgsBuilder() + .add(key) + .add(options.toArgs()) + .add(flattenMapToGlideStringArray(values)))); + return getThis(); + } + + /** + * Reads entries from the given streams. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keysAndIds An array of Pairs of keys and entry ids to read from. A + * pair is composed of a stream's key and the id of the entry after which the stream + * will be read. + * @return Command Response - A {@literal Map>} with stream keys, to Map of stream-ids, to an array of + * pairings with format [[field, entry], + * [field, entry], ...]. + */ + public T xread(@NonNull Map keysAndIds) { + return xread(keysAndIds, StreamReadOptions.builder().build()); + } + + /** + * Reads entries from the given streams. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keysAndIds An array of Pairs of keys and entry ids to read from. A + * pair is composed of a stream's key and the id of the entry after which the stream + * will be read. + * @param options options detailing how to read the stream {@link StreamReadOptions}. + * @return Command Response - A {@literal Map>} with stream keys, to Map of stream-ids, to an array of + * pairings with format [[field, entry], + * [field, entry], ...]. + */ + public T xread( + @NonNull Map keysAndIds, @NonNull StreamReadOptions options) { + checkTypeOrThrow(keysAndIds); + protobufTransaction.addCommands( + buildCommand( + XRead, + newArgsBuilder() + .add(options.toArgs()) + .add(flattenAllKeysFollowedByAllValues(keysAndIds)))); + return getThis(); + } + + /** + * Trims the stream by evicting older entries. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param options Stream trim options {@link StreamTrimOptions}. + * @return Command Response - The number of entries deleted from the stream. + */ + public T xtrim(@NonNull ArgType key, @NonNull StreamTrimOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XTrim, newArgsBuilder().add(key).add(options.toArgs()))); + return getThis(); + } + + /** + * Returns the number of entries in the stream stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @return Command Response - The number of entries in the stream. If key does not + * exist, return 0. + */ + public T xlen(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(XLen, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param ids An array of entry ids. + * @return Command Response - The number of entries removed from the stream. This number may be + * less than the number of entries in ids, if the specified ids + * don't exist in the stream. + */ + public T xdel(@NonNull ArgType key, @NonNull ArgType[] ids) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(XDel, newArgsBuilder().add(key).add(ids))); + return getThis(); + } + + /** + * Returns stream entries matching a given range of IDs. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @return Command Response - A Map of key to stream entry data, where entry data is + * an array of pairings with format [[field, + * entry], [field, entry], ...]. + */ + public T xrange( + @NonNull ArgType key, @NonNull StreamRange start, @NonNull StreamRange end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XRange, newArgsBuilder().add(key).add(StreamRange.toArgs(start, end)))); + return getThis(); + } + + /** + * Returns stream entries matching a given range of IDs. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param count Maximum count of stream entries to return. + * @return Command Response - A Map of key to stream entry data, where entry data is + * an array of pairings with format [[field, + * entry], [field, entry], ...]. + */ + public T xrange( + @NonNull ArgType key, @NonNull StreamRange start, @NonNull StreamRange end, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XRange, newArgsBuilder().add(key).add(StreamRange.toArgs(start, end, count)))); + return getThis(); + } + + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(ArgType, StreamRange, StreamRange)} but returns the entries in + * reverse order. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @return Command Response - A Map of key to stream entry data, where entry data is + * an array of pairings with format [[field, + * entry], [field, entry], ...]. + */ + public T xrevrange( + @NonNull ArgType key, @NonNull StreamRange end, @NonNull StreamRange start) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XRevRange, newArgsBuilder().add(key).add(StreamRange.toArgs(end, start)))); + return getThis(); + } + + /** + * Returns stream entries matching a given range of IDs in reverse order.
+ * Equivalent to {@link #xrange(ArgType, StreamRange, StreamRange, long)} but returns the entries + * in reverse order. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link StreamRange.IdBound#of} to specify a stream ID. + *
  • Use {@link StreamRange.IdBound#ofExclusive} to specify an exclusive bounded stream + * ID. + *
  • Use {@link StreamRange.InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param count Maximum count of stream entries to return. + * @return Command Response - A Map of key to stream entry data, where entry data is + * an array of pairings with format [[field, entry], [field, entry], ...]. + */ + public T xrevrange( + @NonNull ArgType key, @NonNull StreamRange end, @NonNull StreamRange start, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XRevRange, newArgsBuilder().add(key).add(StreamRange.toArgs(end, start, count)))); + return getThis(); + } + + /** + * Creates a new consumer group uniquely identified by groupname for the stream + * stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The newly created consumer group name. + * @param id Stream entry ID that specifies the last delivered entry in the stream from the new + * group's perspective. The special ID "$" can be used to specify the last entry + * in the stream. + * @return Command Response - OK. + */ + public T xgroupCreate( + @NonNull ArgType key, @NonNull ArgType groupName, @NonNull ArgType id) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XGroupCreate, newArgsBuilder().add(key).add(groupName).add(id))); + return getThis(); + } + + /** + * Creates a new consumer group uniquely identified by groupname for the stream + * stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The newly created consumer group name. + * @param id Stream entry ID that specifies the last delivered entry in the stream from the new + * group's perspective. The special ID "$" can be used to specify the last entry + * in the stream. + * @param options The group options {@link StreamGroupOptions}. + * @return Command Response - OK. + */ + public T xgroupCreate( + @NonNull ArgType key, + @NonNull ArgType groupName, + @NonNull ArgType id, + @NonNull StreamGroupOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XGroupCreate, newArgsBuilder().add(key).add(groupName).add(id).add(options.toArgs()))); + return getThis(); + } + + /** + * Destroys the consumer group groupName for the stream stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The newly created consumer group name. + * @return Command Response - true if the consumer group is destroyed. Otherwise, + * false. + */ + public T xgroupDestroy(@NonNull ArgType key, @NonNull ArgType groupName) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XGroupDestroy, newArgsBuilder().add(key).add(groupName))); + return getThis(); + } + + /** + * Creates a consumer named consumer in the consumer group group for the + * stream stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The newly created consumer. + * @return Command Response - true if the consumer is created. Otherwise, false + * . + */ + public T xgroupCreateConsumer( + @NonNull ArgType key, @NonNull ArgType group, @NonNull ArgType consumer) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XGroupCreateConsumer, newArgsBuilder().add(key).add(group).add(consumer))); + return getThis(); + } + + /** + * Deletes a consumer named consumer in the consumer group group. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The newly created consumer. + * @return Command Response - The number of pending messages the consumer had before + * it was deleted. + */ + public T xgroupDelConsumer( + @NonNull ArgType key, @NonNull ArgType group, @NonNull ArgType consumer) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XGroupDelConsumer, newArgsBuilder().add(key).add(group).add(consumer))); + return getThis(); + } + + /** + * Sets the last delivered ID for a consumer group. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @param id The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @return Command Response - OK. + */ + public T xgroupSetId( + @NonNull ArgType key, @NonNull ArgType groupName, @NonNull ArgType id) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XGroupSetId, newArgsBuilder().add(key).add(groupName).add(id))); + return getThis(); + } + + /** + * Sets the last delivered ID for a consumer group. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @since Valkey 7.0 and above + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @param id The stream entry ID that should be set as the last delivered ID for the consumer + * group. + * @param entriesRead A value representing the number of stream entries already read by the group. + * @return Command Response - OK. + */ + public T xgroupSetId( + @NonNull ArgType key, @NonNull ArgType groupName, @NonNull ArgType id, long entriesRead) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XGroupSetId, + newArgsBuilder() + .add(key) + .add(groupName) + .add(id) + .add(ENTRIES_READ_VALKEY_API) + .add(entriesRead))); + return getThis(); + } + + /** + * Reads entries from the given streams owned by a consumer group. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The Map + * is composed of a stream's key and the id of the entry after which the stream will be read. + * Use the special id of {@literal Map>} + * to receive only new messages. + * @param group The consumer group name. + * @param consumer The newly created consumer. + * @return Command Response - A {@literal Map>} with + * stream keys, to Map of stream-ids, to an array of pairings with format + * [[field, entry], [field, entry], ...]. Returns null if the consumer + * group does not exist. Returns a Map with a value of code>null if the + * stream is empty. + */ + public T xreadgroup( + @NonNull Map keysAndIds, + @NonNull ArgType group, + @NonNull ArgType consumer) { + checkTypeOrThrow(group); + return xreadgroup(keysAndIds, group, consumer, StreamReadGroupOptions.builder().build()); + } + + /** + * Reads entries from the given streams owned by a consumer group. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keysAndIds A Map of keys and entry ids to read from. The Map + * is composed of a stream's key and the id of the entry after which the stream will be read. + * Use the special id of {@literal Map>} to + * receive only new messages. + * @param group The consumer group name. + * @param consumer The newly created consumer. + * @param options Options detailing how to read the stream {@link StreamReadGroupOptions}. + * @return Command Response - A {@literal Map>} with + * stream keys, to Map of stream-ids, to an array of pairings with format + * [[field, entry], [field, entry], ...]. Returns null if the consumer + * group does not exist. Returns a Map with a value of code>null if the + * stream is empty. + */ + public T xreadgroup( + @NonNull Map keysAndIds, + @NonNull ArgType group, + @NonNull ArgType consumer, + @NonNull StreamReadGroupOptions options) { + checkTypeOrThrow(group); + protobufTransaction.addCommands( + buildCommand( + XReadGroup, + newArgsBuilder() + .add(options.toArgs(group, consumer)) + .add(flattenAllKeysFollowedByAllValues(keysAndIds)))); + return getThis(); + } + + /** + * Returns the number of messages that were successfully acknowledged by the consumer group member + * of a stream. This command should be called on a pending message so that such message does not + * get processed again. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @param key The key of the stream. + * @param group The consumer group name. + * @param ids Stream entry ID to acknowledge and purge messages. + * @return Command Response - The number of messages that were successfully acknowledged. + */ + public T xack(@NonNull ArgType key, @NonNull ArgType group, @NonNull ArgType[] ids) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(XAck, newArgsBuilder().add(key).add(group).add(ids))); + return getThis(); + } + + /** + * Returns stream message summary information for pending messages matching a given range of IDs. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @return Command Response - A 2D-array that includes the summary of pending + * messages, with the format + * [NumOfMessages, StartId, EndId, [[Consumer, NumOfMessages], ...], where: + *
    + *
  • NumOfMessages: The total number of pending messages for this consumer + * group. + *
  • StartId: The smallest ID among the pending messages. + *
  • EndId: The greatest ID among the pending messages. + *
  • [[Consumer, NumOfMessages], ...]: A 2D-array of every + * consumer in the consumer group with at least one pending message, and the number of + * pending messages it has. + *
+ */ + public T xpending(@NonNull ArgType key, @NonNull ArgType group) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(XPending, newArgsBuilder().add(key).add(group))); + return getThis(); + } + + /** + * Returns an extended form of stream message information for pending messages matching a given + * range of IDs. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param count Limits the number of messages returned. + * @return Command Response - A 2D-array of 4-tuples containing extended message + * information with the format [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ] + * , where: + *
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has + * still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time + * this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ */ + public T xpending( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull StreamRange start, + @NonNull StreamRange end, + long count) { + checkTypeOrThrow(key); + return xpending(key, group, start, end, count, StreamPendingOptions.builder().build()); + } + + /** + * Returns an extended form of stream message information for pending messages matching a given + * range of IDs. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param start Starting stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MIN} to start with the minimum available ID. + *
+ * + * @param end Ending stream ID bound for range. + *
    + *
  • Use {@link IdBound#of} to specify a stream ID. + *
  • Use {@link IdBound#ofExclusive} to specify an exclusive bounded stream ID. + *
  • Use {@link InfRangeBound#MAX} to end with the maximum available ID. + *
+ * + * @param count Limits the number of messages returned. + * @param options Stream add options {@link StreamPendingOptions}. + * @return Command Response - A 2D-array of 4-tuples containing extended message + * information with the format [[ID, Consumer, TimeElapsed, NumOfDelivered], ... ] + * , where: + *
    + *
  • ID: The ID of the message. + *
  • Consumer: The name of the consumer that fetched the message and has + * still to acknowledge it. We call it the current owner of the message. + *
  • TimeElapsed: The number of milliseconds that elapsed since the last time + * this message was delivered to this consumer. + *
  • NumOfDelivered: The number of times this message was delivered. + *
+ */ + public T xpending( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull StreamRange start, + @NonNull StreamRange end, + long count, + @NonNull StreamPendingOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XPending, newArgsBuilder().add(key).add(group).add(options.toArgs(start, end, count)))); + return getThis(); + } + + /** + * Returns information about the stream stored at key key.
+ * To get more detailed information use {@link #xinfoStreamFull(ArgType)} or {@link + * #xinfoStreamFull(ArgType, int)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * @see valkey.io for details. + * @param key The key of the stream. + * @return Command Response - A Map of stream information for the given key + * . + */ + public T xinfoStream(@NonNull ArgType key) { + protobufTransaction.addCommands(buildCommand(XInfoStream, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns verbose information about the stream stored at key key.
+ * The output is limited by first 10 PEL entries. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * @since Valkey 6.0 and above. + * @see valkey.io for details. + * @param key The key of the stream. + * @return Command Response - A Map of detailed stream information for the given + * key. + */ + public T xinfoStreamFull(@NonNull ArgType key) { + protobufTransaction.addCommands(buildCommand(XInfoStream, newArgsBuilder().add(key).add(FULL))); + return getThis(); + } + + /** + * Returns verbose information about the stream stored at key key.
+ * The output is limited by first 10 PEL entries. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * @since Valkey 6.0 and above. + * @see valkey.io for details. + * @param key The key of the stream. + * @param count The number of stream and PEL entries that are returned. Value of 0 + * means that all entries will be returned. + * @return Command Response - A Map of detailed stream information for the given + * key. + */ + public T xinfoStreamFull(@NonNull ArgType key, int count) { + protobufTransaction.addCommands( + buildCommand( + XInfoStream, + newArgsBuilder().add(key).add(FULL).add(COUNT).add(Integer.toString(count)))); + return getThis(); + } + + /** + * Changes the ownership of a pending message. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @return Command Response - A Map of message entries with the format + * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + */ + public T xclaim( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType[] ids) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XClaim, newArgsBuilder().add(key).add(group).add(consumer).add(minIdleTime).add(ids))); + return getThis(); + } + + /** + * Changes the ownership of a pending message. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @param options Stream claim options {@link StreamClaimOptions}. + * @return Command Response - A Map of message entries with the format + * {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + */ + public T xclaim( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType[] ids, + @NonNull StreamClaimOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XClaim, + newArgsBuilder() + .add(key) + .add(group) + .add(consumer) + .add(minIdleTime) + .add(ids) + .add(options.toArgs()))); + return getThis(); + } + + /** + * Changes the ownership of a pending message. This function returns an array with + * only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @return Command Response - An array of message ids claimed by the consumer. + */ + public T xclaimJustId( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType[] ids) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XClaim, + newArgsBuilder() + .add(key) + .add(group) + .add(consumer) + .add(minIdleTime) + .add(ids) + .add(JUST_ID_VALKEY_API))); + return getThis(); + } + + /** + * Changes the ownership of a pending message. This function returns an array with + * only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name. + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param ids An array of entry ids. + * @param options Stream claim options {@link StreamClaimOptions}. + * @return Command Response - An array of message ids claimed by the consumer. + */ + public T xclaimJustId( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType[] ids, + @NonNull StreamClaimOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + XClaim, + newArgsBuilder() + .add(key) + .add(group) + .add(consumer) + .add(minIdleTime) + .add(ids) + .add(options.toArgs()) + .add(JUST_ID_VALKEY_API))); + return getThis(); + } + + /** + * Returns the list of all consumer groups and their attributes for the stream stored at key + * . + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return Command Response - An Array of Maps, where each mapping + * represents the attributes of a consumer group for the stream at key. + */ + public T xinfoGroups(@NonNull ArgType key) { + protobufTransaction.addCommands(buildCommand(XInfoGroups, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the list of all consumers and their attributes for the given consumer group of the + * stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param groupName The consumer group name. + * @return Command Response - An Array of Maps, where each mapping + * contains the attributes of a consumer for the given consumer group of the stream at + * key. + */ + public T xinfoConsumers(@NonNull ArgType key, @NonNull ArgType groupName) { + protobufTransaction.addCommands( + buildCommand(XInfoConsumers, newArgsBuilder().add(key).add(groupName))); + return getThis(); + } + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @return Command Response - An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the + * values being a 2D list of the field-value pairs in the format + * [[field1, value1], [field2, value2], ...]. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ */ + public T xautoclaim( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType start) { + protobufTransaction.addCommands( + buildCommand( + XAutoClaim, + newArgsBuilder().add(key).add(group).add(consumer).add(minIdleTime).add(start))); + return getThis(); + } + + /** + * Transfers ownership of pending stream entries that match the specified criteria. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count Limits the number of claimed entries to the specified value. + * @return Command Response - An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A mapping of the claimed entries, with the keys being the claimed entry IDs and the + * values being a 2D list of the field-value pairs in the format + * [[field1, value1], [field2, value2], ...]. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ */ + public T xautoclaim( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType start, + long count) { + protobufTransaction.addCommands( + buildCommand( + XAutoClaim, + newArgsBuilder() + .add(key) + .add(group) + .add(consumer) + .add(minIdleTime) + .add(start) + .add(READ_COUNT_VALKEY_API) + .add(count))); + return getThis(); + } + + /** + * Transfers ownership of pending stream entries that match the specified criteria. This command + * uses the JUSTID argument to further specify that the return value should contain a + * list of claimed IDs without their field-value info. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @return Command Response - An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A list of the IDs for the claimed entries. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ */ + public T xautoclaimJustId( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType start) { + protobufTransaction.addCommands( + buildCommand( + XAutoClaim, + newArgsBuilder() + .add(key) + .add(group) + .add(consumer) + .add(minIdleTime) + .add(start) + .add(JUST_ID_VALKEY_API))); + return getThis(); + } + + /** + * Transfers ownership of pending stream entries that match the specified criteria. This command + * uses the JUSTID argument to further specify that the return value should contain a + * list of claimed IDs without their field-value info. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the stream. + * @param group The consumer group name + * @param consumer The group consumer. + * @param minIdleTime The minimum idle time for the message to be claimed. + * @param start Filters the claimed entries to those that have an ID equal or greater than the + * specified value. + * @param count Limits the number of claimed entries to the specified value. + * @return Command Response - An array containing the following elements: + *
    + *
  • A stream ID to be used as the start argument for the next call to XAUTOCLAIM + * . This ID is equivalent to the next ID in the stream after the entries that + * were scanned, or "0-0" if the entire stream was scanned. + *
  • A list of the IDs for the claimed entries. + *
  • If you are using Valkey 7.0.0 or above, the response list will also include a list + * containing the message IDs that were in the Pending Entries List but no longer exist + * in the stream. These IDs are deleted from the Pending Entries List. + *
+ */ + public T xautoclaimJustId( + @NonNull ArgType key, + @NonNull ArgType group, + @NonNull ArgType consumer, + long minIdleTime, + @NonNull ArgType start, + long count) { + protobufTransaction.addCommands( + buildCommand( + XAutoClaim, + newArgsBuilder() + .add(key) + .add(group) + .add(consumer) + .add(minIdleTime) + .add(start) + .add(READ_COUNT_VALKEY_API) + .add(count) + .add(JUST_ID_VALKEY_API))); + return getThis(); + } + + /** + * Returns the remaining time to live of key that has a timeout, in milliseconds. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to return its timeout. + * @return Command Response - TTL in milliseconds. -2 if key does not + * exist, -1 if key exists but has no associated expire. + */ + public T pttl(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(PTTL, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Removes the existing timeout on key, turning the key from volatile (a + * key with an expire set) to persistent (a key that will never expire + * as no timeout is associated). + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to remove the existing timeout on. * @return Command Response - false if key does not exist or does not * have an associated timeout, true if the timeout has been removed. */ - public T persist(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Persist, commandArgs)); + public T persist(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Persist, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the server time. + * + * @see valkey.io for details. + * @return Command Response - The current server time as a String array with two + * elements: A UNIX TIME and the amount of microseconds already elapsed in the + * current second. The returned array is in a [UNIX TIME, Microseconds already elapsed] + * format. + */ + public T time() { + protobufTransaction.addCommands(buildCommand(Time)); + return getThis(); + } + + /** + * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save + * was made since then. + * + * @see valkey.io for details. + * @return Command Response - UNIX TIME of the last DB save executed with success. + */ + public T lastsave() { + protobufTransaction.addCommands(buildCommand(LastSave)); + return getThis(); + } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * @see valkey.io for details. + * @return Command Response - OK. + */ + public T flushall() { + protobufTransaction.addCommands(buildCommand(FlushAll)); + return getThis(); + } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return Command Response - OK. + */ + public T flushall(FlushMode mode) { + protobufTransaction.addCommands(buildCommand(FlushAll, newArgsBuilder().add(mode))); + return getThis(); + } + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * @see valkey.io for details. + * @return Command Response - OK. + */ + public T flushdb() { + protobufTransaction.addCommands(buildCommand(FlushDB)); + return getThis(); + } + + /** + * Deletes all the keys of the currently selected database. This command never fails. + * + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return Command Response - OK. + */ + public T flushdb(FlushMode mode) { + protobufTransaction.addCommands(buildCommand(FlushDB, newArgsBuilder().add(mode))); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the server version. + * + * @see valkey.io for details. + * @return Command Response - A piece of generative computer art along with the current Valkey + * version. + */ + public T lolwut() { + protobufTransaction.addCommands(buildCommand(Lolwut)); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the server version. + * + * @see valkey.io for details. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • On the server version 5, those are length of the line, number of squares + * per row, and number of squares per column. + *
  • On the server version 6, those are number of columns and number of + * lines. + *
  • On other versions parameters are ignored. + *
+ * + * @return Command Response - A piece of generative computer art along with the current Valkey + * version. + */ + public T lolwut(int @NonNull [] parameters) { + protobufTransaction.addCommands(buildCommand(Lolwut, newArgsBuilder().add(parameters))); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the server version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @return Command Response - A piece of generative computer art along with the current Valkey + * version. + */ + public T lolwut(int version) { + protobufTransaction.addCommands( + buildCommand(Lolwut, newArgsBuilder().add(VERSION_VALKEY_API).add(version))); + return getThis(); + } + + /** + * Displays a piece of generative computer art and the server version. + * + * @apiNote Versions 5 and 6 produce graphical things. + * @see valkey.io for details. + * @param version Version of computer art to generate. + * @param parameters Additional set of arguments in order to change the output: + *
    + *
  • For version 5, those are length of the line, number of squares per row, + * and number of squares per column. + *
  • For version 6, those are number of columns and number of lines. + *
+ * + * @return Command Response - A piece of generative computer art along with the current Valkey + * version. + */ + public T lolwut(int version, int @NonNull [] parameters) { + protobufTransaction.addCommands( + buildCommand( + Lolwut, newArgsBuilder().add(VERSION_VALKEY_API).add(version).add(parameters))); + return getThis(); + } + + /** + * Returns the number of keys in the currently selected database. + * + * @see valkey.io for details. + * @return Command Response - The number of keys in the currently selected database. + */ + public T dbsize() { + protobufTransaction.addCommands(buildCommand(DBSize)); + return getThis(); + } + + /** + * Returns the string representation of the type of the value stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to check its data type. + * @return Command Response - If the key exists, the type of the stored value is + * returned. Otherwise, a "none" string is returned. + */ + public T type(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Type, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns a random key from the currently selected database. * + * + * @see valkey.io for details. + * @return Command Response - A random key from the database. + */ + public T randomKey() { + protobufTransaction.addCommands(buildCommand(RandomKey)); + return getThis(); + } + + /** + * Renames key to newKey.
+ * If newKey already exists it is overwritten. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to rename. + * @param newKey The new name of the key. + * @return Command Response - If the key was successfully renamed, returns OK + * . If key does not exist, the transaction fails with an error. + */ + public T rename(@NonNull ArgType key, @NonNull ArgType newKey) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Rename, newArgsBuilder().add(key).add(newKey))); + return getThis(); + } + + /** + * Renames key to newKey if newKey does not yet exist. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key to rename. + * @param newKey The new key name. + * @return Command Response - true if key was renamed to newKey + * , false if newKey already exists. + */ + public T renamenx(@NonNull ArgType key, @NonNull ArgType newKey) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(RenameNX, newArgsBuilder().add(key).add(newKey))); + return getThis(); + } + + /** + * Inserts element in the list at key either before or after the + * pivot. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list. + * @param position The relative position to insert into - either {@link InsertPosition#BEFORE} or + * {@link InsertPosition#AFTER} the pivot. + * @param pivot An element of the list. + * @param element The new element to insert. + * @return Command Response - The list length after a successful insert operation.
+ * If the key doesn't exist returns -1.
+ * If the pivot wasn't found, returns 0. + */ + public T linsert( + @NonNull ArgType key, + @NonNull InsertPosition position, + @NonNull ArgType pivot, + @NonNull ArgType element) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(LInsert, newArgsBuilder().add(key).add(position).add(pivot).add(element))); + return getThis(); + } + + /** + * Pops an element from the tail of the first list that is non-empty, with the given keys + * being checked in the order that they are given.
+ * Blocks the connection when there are no elements to pop from any of the given lists. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @apiNote BRPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + * @param keys The keys of the lists to pop from. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A two-element array containing the key + * from which the element was popped and the value of the popped element, + * formatted as [key, value]. If no element could be popped and the timeout + * expired, returns null. + */ + public T brpop(@NonNull ArgType[] keys, double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(BRPop, newArgsBuilder().add(keys).add(timeout))); + return getThis(); + } + + /** + * Inserts all the specified values at the head of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return Command Response - The length of the list after the push operation. + */ + public T lpushx(@NonNull ArgType key, @NonNull ArgType[] elements) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(LPushX, newArgsBuilder().add(key).add(elements))); + return getThis(); + } + + /** + * Inserts all the specified values at the tail of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return Command Response - The length of the list after the push operation. + */ + public T rpushx(@NonNull ArgType key, @NonNull ArgType[] elements) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(RPushX, newArgsBuilder().add(key).add(elements))); + return getThis(); + } + + /** + * Pops an element from the head of the first list that is non-empty, with the given keys + * being checked in the order that they are given.
+ * Blocks the connection when there are no elements to pop from any of the given lists. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @apiNote BLPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + * @param keys The keys of the lists to pop from. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A two-element array containing the key + * from which the element was popped and the value of the popped element, + * formatted as [key, value]. If no element could be popped and the timeout + * expired, returns null. + */ + public T blpop(@NonNull ArgType[] keys, double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(BLPop, newArgsBuilder().add(keys).add(timeout))); + return getThis(); + } + + /** + * Returns the specified range of elements in the sorted set stored at key.
+ * ZRANGE can perform different types of range queries: by index (rank), by the + * score, or by lexicographical order.
+ * To get the elements with their scores, see {@link #zrangeWithScores}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
+ *
    + *
  • For range queries by index (rank), use {@link RangeByIndex}. + *
  • For range queries by lexicographical order, use {@link RangeByLex}. + *
  • For range queries by score, use {@link RangeByScore}. + *
+ * + * @param reverse If true, reverses the sorted set, with index 0 as the element with the highest + * score. + * @return Command Response - An array of elements within the specified range. If + * key does not exist, it is treated as an empty sorted set, and the command + * returns an empty array. + */ + public T zrange(@NonNull ArgType key, @NonNull RangeQuery rangeQuery, boolean reverse) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZRange, + newArgsBuilder() + .add(key) + .add(RangeOptions.createZRangeBaseArgs(rangeQuery, reverse, false)))); + return getThis(); + } + + /** + * Returns the specified range of elements in the sorted set stored at key.
+ * ZRANGE can perform different types of range queries: by index (rank), by the + * score, or by lexicographical order.
+ * To get the elements with their scores, see {@link #zrangeWithScores}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
+ *
    + *
  • For range queries by index (rank), use {@link RangeByIndex}. + *
  • For range queries by lexicographical order, use {@link RangeByLex}. + *
  • For range queries by score, use {@link RangeByScore}. + *
+ * + * @return Command Response - An array of elements within the specified range. If + * key does not exist, it is treated as an empty sorted set, and the command + * returns an empty array. + */ + public T zrange(@NonNull ArgType key, @NonNull RangeQuery rangeQuery) { + return getThis().zrange(key, rangeQuery, false); + } + + /** + * Returns the specified range of elements with their scores in the sorted set stored at key + * . Similar to {@link #zrange} but with a WITHSCORE flag. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
+ *
    + *
  • For range queries by index (rank), use {@link RangeByIndex}. + *
  • For range queries by score, use {@link RangeByScore}. + *
+ * + * @param reverse If true, reverses the sorted set, with index 0 as the element with the highest + * score. + * @return Command Response - A Map of elements and their scores within the specified + * range. If key does not exist, it is treated as an empty sorted set, and the + * command returns an empty Map. + */ + public T zrangeWithScores( + @NonNull ArgType key, @NonNull ScoredRangeQuery rangeQuery, boolean reverse) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + ZRange, + newArgsBuilder() + .add(key) + .add(RangeOptions.createZRangeBaseArgs(rangeQuery, reverse, true)))); + return getThis(); + } + + /** + * Returns the specified range of elements with their scores in the sorted set stored at key + * . Similar to {@link #zrange} but with a WITHSCORE flag. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param rangeQuery The range query object representing the type of range query to perform.
+ *
    + *
  • For range queries by index (rank), use {@link RangeByIndex}. + *
  • For range queries by score, use {@link RangeByScore}. + *
+ * + * @return Command Response - A Map of elements and their scores within the specified + * range. If key does not exist, it is treated as an empty sorted set, and the + * command returns an empty Map. + */ + public T zrangeWithScores(@NonNull ArgType key, @NonNull ScoredRangeQuery rangeQuery) { + return zrangeWithScores(key, rangeQuery, false); + } + + /** + * Pops a member-score pair from the first non-empty sorted set, with the given keys + * being checked in the order they are provided. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop the member with the lowest/highest score accordingly. + * @return Command Response - A two-element array containing the key name of the set + * from which the element was popped, and a member-score Map of the popped + * element.
+ * If no member could be popped, returns null. + */ + public T zmpop(@NonNull ArgType[] keys, @NonNull ScoreFilter modifier) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(ZMPop, newArgsBuilder().add(keys.length).add(keys).add(modifier))); + return getThis(); + } + + /** + * Pops multiple member-score pairs from the first non-empty sorted set, with the given keys + * being checked in the order they are provided. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @since Valkey 7.0 and above. + * @see valkey.io for more details. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param count The number of elements to pop. + * @return Command Response - A two-element array containing the key name of the set + * from which elements were popped, and a member-score Map of the popped + * elements.
+ * If no member could be popped, returns null. + */ + public T zmpop(@NonNull ArgType[] keys, @NonNull ScoreFilter modifier, long count) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + ZMPop, + newArgsBuilder() + .add(keys.length) + .add(keys) + .add(modifier) + .add(COUNT_VALKEY_API) + .add(count))); + return getThis(); + } + + /** + * Blocks the connection until it pops and returns a member-score pair from the first non-empty + * sorted set, with the given keys being checked in the order they are provided.
+ * BZMPOP is the blocking variant of {@link #zmpop(ArgType[], ScoreFilter)}. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @apiNote BZMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A two-element array containing the key name of the set + * from which an element was popped, and a member-score Map of the popped + * elements.
+ * If no member could be popped and the timeout expired, returns null. + */ + public T bzmpop( + @NonNull ArgType[] keys, @NonNull ScoreFilter modifier, double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + BZMPop, newArgsBuilder().add(timeout).add(keys.length).add(keys).add(modifier))); + return getThis(); + } + + /** + * Blocks the connection until it pops and returns multiple member-score pairs from the first + * non-empty sorted set, with the given keys being checked in the order they are + * provided.
+ * BZMPOP is the blocking variant of {@link #zmpop(ArgType[], ScoreFilter, long)}. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @apiNote BZMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + * @param keys The keys of the sorted sets. + * @param modifier The element pop criteria - either {@link ScoreFilter#MIN} or {@link + * ScoreFilter#MAX} to pop members with the lowest/highest scores accordingly. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @param count The number of elements to pop. + * @return Command Response - A two-element array containing the key name of the set + * from which elements were popped, and a member-score Map of the popped + * elements.
+ * If no members could be popped and the timeout expired, returns null. + */ + public T bzmpop( + @NonNull ArgType[] keys, @NonNull ScoreFilter modifier, double timeout, long count) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + BZMPop, + newArgsBuilder() + .add(timeout) + .add(keys.length) + .add(keys) + .add(modifier) + .add(COUNT_VALKEY_API) + .add(count))); + return getThis(); + } + + /** + * Adds all elements to the HyperLogLog data structure stored at the specified key. + *
+ * Creates a new structure if the key does not exist. + * + *

When no elements are provided, and key exists and is a + * HyperLogLog, then no operation is performed. If key does not exist, then the + * HyperLogLog structure is created. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the HyperLogLog data structure to add elements into. + * @param elements An array of members to add to the HyperLogLog stored at key + * . + * @return Command Response - If the HyperLogLog is newly created, or if the HyperLogLog + * approximated cardinality is altered, then returns 1. Otherwise, returns + * 0. + */ + public T pfadd(@NonNull ArgType key, @NonNull ArgType[] elements) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(PfAdd, newArgsBuilder().add(key).add(elements))); + return getThis(); + } + + /** + * Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or + * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys The keys of the HyperLogLog data structures to be analyzed. + * @return Command Response - The approximated cardinality of given HyperLogLog data structures. + *
+ * The cardinality of a key that does not exist is 0. + */ + public T pfcount(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(PfCount, newArgsBuilder().add(keys))); + return getThis(); + } + + /** + * Merges multiple HyperLogLog values into a unique value.
+ * If the destination variable exists, it is treated as one of the source HyperLogLog data sets, + * otherwise a new HyperLogLog is created. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param destination The key of the destination HyperLogLog where the merged data sets will be + * stored. + * @param sourceKeys The keys of the HyperLogLog structures to be merged. + * @return Command Response - OK. + */ + public T pfmerge(@NonNull ArgType destination, @NonNull ArgType[] sourceKeys) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand(PfMerge, newArgsBuilder().add(destination).add(sourceKeys))); + return getThis(); + } + + /** + * Returns the internal encoding for the server object stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the object to get the internal encoding of. + * @return Command response - If key exists, returns the internal encoding of the + * object stored at key as a String. Otherwise, return null + * . + */ + public T objectEncoding(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ObjectEncoding, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the logarithmic access frequency counter of a server object stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the object to get the logarithmic access frequency counter + * of. + * @return Command response - If key exists, returns the logarithmic access frequency + * counter of the object stored at key as a Long. Otherwise, returns + * null. + */ + public T objectFreq(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ObjectFreq, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the time in seconds since the last access to the value stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the object to get the idle time of. + * @return Command response - If key exists, returns the idle time in seconds. + * Otherwise, returns null. + */ + public T objectIdletime(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ObjectIdleTime, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns the reference count of the object stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the object to get the reference count of. + * @return Command response - If key exists, returns the reference count of the + * object stored at key as a Long. Otherwise, returns null + * . + */ + public T objectRefcount(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ObjectRefCount, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Updates the last access time of specified keys. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys The keys to update last access time. + * @return Command Response - The number of keys that were updated. + */ + public T touch(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(Touch, newArgsBuilder().add(keys))); + return getThis(); + } + + /** + * Copies the value stored at the source to the destination key. When + * replace is true, removes the destination key first if it already + * exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param replace If the destination key should be removed before copying the value to it. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public T copy(@NonNull ArgType source, @NonNull ArgType destination, boolean replace) { + checkTypeOrThrow(source); + protobufTransaction.addCommands( + buildCommand( + Copy, + newArgsBuilder().add(source).add(destination).addIf(REPLACE_VALKEY_API, replace))); + return getThis(); + } + + /** + * Copies the value stored at the source to the destination key if the + * destination key does not yet exist. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public T copy(@NonNull ArgType source, @NonNull ArgType destination) { + return copy(source, destination, false); + } + + /** + * Serialize the value stored at key in a Valkey-specific format and return it to the + * user. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @return Command Response - The serialized value of a set. If key does not exist, + * null will be returned. + */ + public T dump(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Dump, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Create a key associated with a value that is obtained by + * deserializing the provided serialized value (obtained via {@link #dump}). + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @param ttl The expiry time (in milliseconds). If 0, the key will + * persist. + * @param value The serialized value. + * @return Command Response - Return OK if successfully create a key + * with a value. + */ + public T restore(@NonNull ArgType key, long ttl, @NonNull byte[] value) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Restore, newArgsBuilder().add(key).add(ttl).add(value))); + return getThis(); + } + + /** + * Create a key associated with a value that is obtained by + * deserializing the provided serialized value (obtained via {@link #dump}). + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @param ttl The expiry time (in milliseconds). If 0, the key will + * persist. + * @param value The serialized value. + * @param restoreOptions The restore options. See {@link RestoreOptions}. + * @return Command Response - Return OK if successfully create a key + * with a value. + */ + public T restore( + @NonNull ArgType key, + long ttl, + @NonNull byte[] value, + @NonNull RestoreOptions restoreOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Restore, newArgsBuilder().add(key).add(ttl).add(value).add(restoreOptions))); + return getThis(); + } + + /** + * Counts the number of set bits (population counting) in a string stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @return Command Response - The number of set bits in the string. Returns zero if the key is + * missing as it is treated as an empty string. + */ + public T bitcount(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(BitCount, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting byte offset. + * @param end The ending byte offset. + * @return Command Response - The number of set bits in the string byte interval specified by + * start and end. Returns zero if the key is missing as it is + * treated as an empty string. + */ + public T bitcount(@NonNull ArgType key, long start, long end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(BitCount, newArgsBuilder().add(key).add(start).add(end))); + return getThis(); + } + + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @since Valkey 7.0 and above + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key for the string to count the set bits of. + * @param start The starting offset. + * @param end The ending offset. + * @param options The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return Command Response - The number of set bits in the string interval specified by + * start, end, and options. Returns zero if the key is + * missing as it is treated as an empty string. + */ + public T bitcount( + @NonNull ArgType key, long start, long end, @NonNull BitmapIndexType options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(BitCount, newArgsBuilder().add(key).add(start).add(end).add(options))); + return getThis(); + } + + /** + * Adds geospatial members with their positions to the specified sorted set stored at key + * .
+ * If a member is already a part of the sorted set, its position is updated. + * + * @see valkey.io for more details. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @param key The key of the sorted set. + * @param membersToGeospatialData A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @param options The GeoAdd options - see {@link GeoAddOptions} + * @return Command Response - The number of elements added to the sorted set. If changed + * is set to true in the options, returns the number of elements updated + * in the sorted set. + */ + public T geoadd( + @NonNull ArgType key, + @NonNull Map membersToGeospatialData, + @NonNull GeoAddOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + GeoAdd, + newArgsBuilder() + .add(key) + .add(options.toArgs()) + .add(mapGeoDataToGlideStringArray(membersToGeospatialData)))); + return getThis(); + } + + /** + * Adds geospatial members with their positions to the specified sorted set stored at key + * .
+ * If a member is already a part of the sorted set, its position is updated.
+ * To perform a geoadd operation while specifying optional parameters, use {@link + * #geoadd(ArgType, Map, GeoAddOptions)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param membersToGeospatialData A mapping of member names to their corresponding positions - see + * {@link GeospatialData}. The command will report an error when the user attempts to index + * coordinates outside the specified ranges. + * @return Command Response - The number of elements added to the sorted set. + */ + public T geoadd( + @NonNull ArgType key, @NonNull Map membersToGeospatialData) { + return geoadd(key, membersToGeospatialData, new GeoAddOptions(false)); + } + + /** + * Returns the positions (longitude,latitude) of all the specified members of the + * geospatial index represented by the sorted set at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members The members for which to get the positions. + * @return Command Response - A 2D array which represent positions (longitude and + * latitude) corresponding to the given members. If a member does not exist, its position will + * be null. + */ + public T geopos(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(GeoPos, newArgsBuilder().add(key).add(members))); + return getThis(); + } + + /** + * Returns the distance between member1 and member2 saved in the + * geospatial index stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member1 The name of the first member. + * @param member2 The name of the second member. + * @param geoUnit The unit of distance measurement - see {@link GeoUnit}. + * @return Command Response - The distance between member1 and member2. + * If one or both members do not exist or if the key does not exist returns null. + */ + public T geodist( + @NonNull ArgType key, + @NonNull ArgType member1, + @NonNull ArgType member2, + @NonNull GeoUnit geoUnit) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + GeoDist, + newArgsBuilder().add(key).add(member1).add(member2).add(geoUnit.getValkeyAPI()))); + return getThis(); + } + + /** + * Returns the distance between member1 and member2 saved in the + * geospatial index stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param member1 The name of the first member. + * @param member2 The name of the second member. + * @return Command Response - The distance between member1 and member2. + * If one or both members do not exist or if the key does not exist returns null. + * The default unit is {@link GeoUnit#METERS}. + */ + public T geodist( + @NonNull ArgType key, @NonNull ArgType member1, @NonNull ArgType member2) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(GeoDist, newArgsBuilder().add(key).add(member1).add(member2))); + return getThis(); + } + + /** + * Returns the GeoHash strings representing the positions of all the specified + * members in the sorted set stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key of the sorted set. + * @param members The array of members whose GeoHash strings are to be retrieved. + * @return Command Response - An array of GeoHash strings representing the positions + * of the specified members stored at key. If a member does not exist in the + * sorted set, a null value is returned for that member. + */ + public T geohash(@NonNull ArgType key, @NonNull ArgType[] members) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(GeoHash, newArgsBuilder().add(key).add(members))); + return getThis(); + } + + /** + * Loads a library to Valkey. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param libraryCode The source code that implements the library. + * @param replace Whether the given library should overwrite a library with the same name if it + * already exists. + * @return Command Response - The library name that was loaded. + */ + public T functionLoad(@NonNull ArgType libraryCode, boolean replace) { + checkTypeOrThrow(libraryCode); + protobufTransaction.addCommands( + buildCommand(FunctionLoad, newArgsBuilder().addIf(REPLACE, replace).add(libraryCode))); + return getThis(); + } + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Command Response - Info about all libraries and their functions. + */ + public T functionList(boolean withCode) { + protobufTransaction.addCommands( + buildCommand(FunctionList, newArgsBuilder().addIf(WITH_CODE_VALKEY_API, withCode))); + return getThis(); + } + + /** + * Returns information about the functions and libraries. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param libNamePattern A wildcard pattern for matching library names. + * @param withCode Specifies whether to request the library code from the server or not. + * @return Command Response - Info about queried libraries and their functions. + */ + public T functionList(@NonNull ArgType libNamePattern, boolean withCode) { + checkTypeOrThrow(libNamePattern); + protobufTransaction.addCommands( + buildCommand( + FunctionList, + newArgsBuilder() + .add(LIBRARY_NAME_VALKEY_API) + .add(libNamePattern) + .addIf(WITH_CODE_VALKEY_API, withCode))); + return getThis(); + } + + /** + * Invokes a previously loaded function. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param function The function name. + * @param keys An array of key arguments accessed by the function. To ensure the + * correct execution of functions, both in standalone and clustered deployments, all names of + * keys that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return Command Response - The invoked function's return value. + */ + public T fcall( + @NonNull ArgType function, @NonNull ArgType[] keys, @NonNull ArgType[] arguments) { + checkTypeOrThrow(function); + protobufTransaction.addCommands( + buildCommand( + FCall, newArgsBuilder().add(function).add(keys.length).add(keys).add(arguments))); + return getThis(); + } + + /** + * Invokes a previously loaded read-only function. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return Command Response - The invoked function's return value. + */ + public T fcall(@NonNull ArgType function, @NonNull ArgType[] arguments) { + return fcall(function, createArray(), arguments); + } + + /** + * Invokes a previously loaded read-only function. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param function The function name. + * @param keys An array of key arguments accessed by the function. To ensure the + * correct execution of functions, both in standalone and clustered deployments, all names of + * keys that a function accesses must be explicitly provided as keys. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return Command Response - The invoked function's return value. + */ + public T fcallReadOnly( + @NonNull ArgType function, @NonNull ArgType[] keys, @NonNull ArgType[] arguments) { + checkTypeOrThrow(function); + protobufTransaction.addCommands( + buildCommand( + FCallReadOnly, + newArgsBuilder().add(function).add(keys.length).add(keys).add(arguments))); + return getThis(); + } + + /** + * Invokes a previously loaded function. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param function The function name. + * @param arguments An array of function arguments. arguments + * should not represent names of keys. + * @return Command Response - The invoked function's return value. + */ + public T fcallReadOnly(@NonNull ArgType function, @NonNull ArgType[] arguments) { + return fcallReadOnly(function, createArray(), arguments); + } + + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return Command Response - A Map with two keys: + *

    + *
  • running_script with information about the running script. + *
  • engines with information about available engines and their stats. + *
+ */ + public T functionStats() { + protobufTransaction.addCommands(buildCommand(FunctionStats)); + return getThis(); + } + + /** + * Returns the serialized payload of all loaded libraries. The command will be routed to a random + * node. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return Command Response - The serialized payload of all loaded libraries. + */ + public T functionDump() { + protobufTransaction.addCommands(buildCommand(FunctionDump)); + return getThis(); + } + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}. The command + * will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @return Command Response - OK. + */ + public T functionRestore(@NonNull byte[] payload) { + protobufTransaction.addCommands(buildCommand(FunctionRestore, newArgsBuilder().add(payload))); + return getThis(); + } + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}. The command + * will be routed to all primary nodes. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return Command Response - OK. + */ + public T functionRestore(@NonNull byte[] payload, @NonNull FunctionRestorePolicy policy) { + protobufTransaction.addCommands( + buildCommand(FunctionRestore, newArgsBuilder().add(payload).add(policy))); + return getThis(); + } + + /** + * Sets or clears the bit at offset in the string value stored at key. + * The offset is a zero-based index, with 0 being the first element of + * the list, 1 being the next element, and so on. The offset must be + * less than 2^32 and greater than or equal to 0. If a key is + * non-existent then the bit at offset is set to value and the preceding + * bits are set to 0. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param offset The index of the bit to be set. + * @param value The bit value to set at offset. The value must be 0 or + * 1. + * @return Command Response - The bit value that was previously stored at offset. + */ + public T setbit(@NonNull ArgType key, long offset, long value) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SetBit, newArgsBuilder().add(key).add(offset).add(value))); + return getThis(); + } + + /** + * Returns the bit value at offset in the string value stored at key. + * offset should be greater than or equal to zero. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param offset The index of the bit to return. + * @return Command Response - The bit at offset of the string. Returns zero if the key is empty or + * if the positive offset exceeds the length of the string. + */ + public T getbit(@NonNull ArgType key, long offset) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(GetBit, newArgsBuilder().add(key).add(offset))); + return getThis(); + } + + /** + * Blocks the connection until it pops one or more elements from the first non-empty list from the + * provided keys. BLMPOP is the blocking variant of {@link + * #lmpop(ArgType[], ListDirection, Long)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @apiNote BLMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param keys The list of provided key names. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param count The maximum number of popped elements. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A Map of key names arrays of popped + * elements.
+ * If no member could be popped and the timeout expired, returns null. + */ + public T blmpop( + @NonNull ArgType[] keys, + @NonNull ListDirection direction, + @NonNull Long count, + double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + BLMPop, + newArgsBuilder() + .add(timeout) + .add(keys.length) + .add(keys) + .add(direction) + .add(COUNT_FOR_LIST_VALKEY_API) + .add(count))); + return getThis(); + } + + /** + * Blocks the connection until it pops one element from the first non-empty list from the provided + * keys. BLMPOP is the blocking variant of {@link #lmpop(ArgType[], + * ListDirection)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @apiNote BLMPOP is a client blocking command, see Blocking + * Commands for more details and best practices. + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param keys The list of provided key names. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - A Map of key names arrays of popped + * elements.
+ * If no member could be popped and the timeout expired, returns null. + */ + public T blmpop( + @NonNull ArgType[] keys, @NonNull ListDirection direction, double timeout) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + BLMPop, newArgsBuilder().add(timeout).add(keys.length).add(keys).add(direction))); + return getThis(); + } + + /** + * Returns the position of the first bit matching the given bit value. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @return Command Response - The position of the first occurrence matching bit in + * the binary value of the string held at key. If bit is not found, + * a -1 is returned. + */ + public T bitpos(@NonNull ArgType key, long bit) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(BitPos, newArgsBuilder().add(key).add(bit))); + return getThis(); + } + + /** + * Returns the position of the first bit matching the given bit value. The offset + * start is a zero-based index, with 0 being the first byte of the list, + * 1 being the next byte and so on. These offsets can also be negative numbers + * indicating offsets starting at the end of the list, with -1 being the last byte of + * the list, -2 being the penultimate, and so on. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @return Command Response - The position of the first occurrence beginning at the start + * offset of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + */ + public T bitpos(@NonNull ArgType key, long bit, long start) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(BitPos, newArgsBuilder().add(key).add(bit).add(start))); + return getThis(); + } + + /** + * Returns the position of the first bit matching the given bit value. The offsets + * start and end are zero-based indexes, with 0 being the + * first byte of the list, 1 being the next byte and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being + * the last byte of the list, -2 being the penultimate, and so on. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @return Command Response - The position of the first occurrence from the start to + * the end offsets of the bit in the binary value of the string held + * at key. If bit is not found, a -1 is returned. + */ + public T bitpos(@NonNull ArgType key, long bit, long start, long end) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(BitPos, newArgsBuilder().add(key).add(bit).add(start).add(end))); + return getThis(); + } + + /** + * Returns the position of the first bit matching the given bit value. The offset + * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, + * start==0 and end==2 means to look at the first three bits. If BYTE is + * specified, start==0 and end==2 means to look at the first three bytes + * The offsets are zero-based indexes, with 0 being the first element of the list, + * 1 being the next, and so on. These offsets can also be negative numbers indicating + * offsets starting at the end of the list, with -1 being the last element of the + * list, -2 being the penultimate, and so on. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param bit The bit value to match. The value must be 0 or 1. + * @param start The starting offset. + * @param end The ending offset. + * @param offsetType The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return Command Response - The position of the first occurrence from the start to + * the end offsets of the bit in the binary value of the string held + * at key. If bit is not found, a -1 is returned. + */ + public T bitpos( + @NonNull ArgType key, long bit, long start, long end, @NonNull BitmapIndexType offsetType) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + BitPos, newArgsBuilder().add(key).add(bit).add(start).add(end).add(offsetType))); + return getThis(); + } + + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the + * result in the destination. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param bitwiseOperation The bitwise operation to perform. + * @param destination The key that will store the resulting string. + * @param keys The list of keys to perform the bitwise operation on. + * @return Command Response - The size of the string stored in destination. + */ + public T bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull ArgType destination, + @NonNull ArgType[] keys) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand(BitOp, newArgsBuilder().add(bitwiseOperation).add(destination).add(keys))); + return getThis(); + } + + /** + * Pops one or more elements from the first non-empty list from the provided keys + * . + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @param count The maximum number of popped elements. + * @return Command Response - A Map of key name mapped arrays of popped + * elements. + */ + public T lmpop( + @NonNull ArgType[] keys, @NonNull ListDirection direction, @NonNull Long count) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand( + LMPop, + newArgsBuilder() + .add(keys.length) + .add(keys) + .add(direction) + .add(COUNT_FOR_LIST_VALKEY_API) + .add(count))); + return getThis(); + } + + /** + * Pops one element from the first non-empty list from the provided keys. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * ListDirection}. + * @return Command Response - A Map of key name mapped array of the + * popped element. + */ + public T lmpop(@NonNull ArgType[] keys, @NonNull ListDirection direction) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands( + buildCommand(LMPop, newArgsBuilder().add(keys.length).add(keys).add(direction))); + return getThis(); + } + + /** + * Sets the list element at index to element.
+ * The index is zero-based, so 0 means the first element, 1 the second + * element and so on. Negative indices can be used to designate elements starting at the tail of + * the list. Here, -1 means the last element, -2 means the penultimate + * and so forth. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list. + * @param index The index of the element in the list to be set. + * @return Command Response - OK. + */ + public T lset(@NonNull ArgType key, long index, @NonNull ArgType element) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(LSet, newArgsBuilder().add(key).add(index).add(element))); + return getThis(); + } + + /** + * Atomically pops and removes the left/right-most element to the list stored at source + * depending on whereFrom, and pushes the element at the first/last element + * of the list stored at destination depending on whereFrom. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param whereFrom The {@link ListDirection} the element should be removed from. + * @param whereTo The {@link ListDirection} the element should be added to. + * @return Command Response - The popped element or null if source does + * not exist. + */ + public T lmove( + @NonNull ArgType source, + @NonNull ArgType destination, + @NonNull ListDirection whereFrom, + @NonNull ListDirection whereTo) { + checkTypeOrThrow(source); + protobufTransaction.addCommands( + buildCommand( + LMove, newArgsBuilder().add(source).add(destination).add(whereFrom).add(whereTo))); + return getThis(); + } + + /** + * Blocks the connection until it atomically pops and removes the left/right-most element to the + * list stored at source depending on whereFrom, and pushes the element + * at the first/last element of the list stored at destination depending on + * whereFrom.
+ * BLMove is the blocking variant of {@link #lmove(ArgType, ArgType, ListDirection, + * ListDirection)}. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @apiNote BLMove is a client blocking command, see Blocking + * Commands for more details and best practices. + * @see valkey.io for details. + * @param source The key to the source list. + * @param destination The key to the destination list. + * @param whereFrom The {@link ListDirection} the element should be removed from. + * @param whereTo The {@link ListDirection} the element should be added to. + * @param timeout The number of seconds to wait for a blocking operation to complete. A value of + * 0 will block indefinitely. + * @return Command Response - The popped element or null if source does + * not exist or if the operation timed-out. + */ + public T blmove( + @NonNull ArgType source, + @NonNull ArgType destination, + @NonNull ListDirection whereFrom, + @NonNull ListDirection whereTo, + double timeout) { + checkTypeOrThrow(source); + protobufTransaction.addCommands( + buildCommand( + BLMove, + newArgsBuilder() + .add(source) + .add(destination) + .add(whereFrom) + .add(whereTo) + .add(timeout))); + return getThis(); + } + + /** + * Returns a random element from the set value stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key from which to retrieve the set member. + * @return Command Response - A random element from the set, or null if key + * does not exist. + */ + public T srandmember(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SRandMember, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Returns random elements from the set value stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key from which to retrieve the set members. + * @param count The number of elements to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates.
+ * @return Command Response - An array of elements from the set, or an empty + * array if key does not exist. + */ + public T srandmember(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SRandMember, newArgsBuilder().add(key).add(count))); + return getThis(); + } + + /** + * Removes and returns one random member from the set stored at key. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @return Command Response - The value of the popped member.
+ * If key does not exist, null will be returned. + */ + public T spop(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SPop, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Removes and returns up to count random members from the set stored at key + * , depending on the set's length. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @param count The count of the elements to pop from the set. + * @return Command Response - A Set of popped elements will be returned depending on + * the set's length.
+ * If key does not exist, an empty Set will be returned. + */ + public T spopCount(@NonNull ArgType key, long count) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SPop, newArgsBuilder().add(key).add(count))); + return getThis(); + } + + /** + * Reads or modifies the array of bits representing the string that is held at key + * based on the specified subCommands. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param subCommands The subCommands to be performed on the binary value of the string at + * key, which could be any of the following: + *
    + *
  • {@link BitFieldGet}. + *
  • {@link BitFieldSet}. + *
  • {@link BitFieldIncrby}. + *
  • {@link BitFieldOverflow}. + *
+ * + * @return Command Response - An array of results from the executed subcommands. + *
    + *
  • {@link BitFieldGet} returns the value in {@link Offset} or {@link OffsetMultiplier}. + *
  • {@link BitFieldSet} returns the old value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldIncrby} returns the new value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldOverflow} determines the behaviour of SET and + * INCRBY when an overflow occurs. OVERFLOW does not return a value + * and does not contribute a value to the array response. + *
+ */ + public T bitfield(@NonNull ArgType key, @NonNull BitFieldSubCommands[] subCommands) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(BitField, newArgsBuilder().add(key).add(createBitFieldArgs(subCommands)))); + return getThis(); + } + + /** + * Reads the array of bits representing the string that is held at key based on the + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Valkey 6.0 and above + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the string. + * @param subCommands The GET subCommands to be performed. + * @return Command Response - An array of results from the GET + * subcommands. + */ + public T bitfieldReadOnly( + @NonNull ArgType key, @NonNull BitFieldReadOnlySubCommands[] subCommands) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + BitFieldReadOnly, newArgsBuilder().add(key).add(createBitFieldArgs(subCommands)))); + return getThis(); + } + + /** + * Deletes all function libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @return Command Response - OK. + */ + public T functionFlush() { + protobufTransaction.addCommands(buildCommand(FunctionFlush)); + return getThis(); + } + + /** + * Deletes all function libraries. + * + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return Command Response - OK. + */ + public T functionFlush(@NonNull FlushMode mode) { + protobufTransaction.addCommands(buildCommand(FunctionFlush, newArgsBuilder().add(mode))); + return getThis(); + } + + /** + * Deletes a library and all its functions. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param libName The library name to delete. + * @return Command Response - OK. + */ + public T functionDelete(@NonNull ArgType libName) { + checkTypeOrThrow(libName); + protobufTransaction.addCommands(buildCommand(FunctionDelete, newArgsBuilder().add(libName))); + return getThis(); + } + + /** + * Returns the longest common subsequence between strings stored at key1 and + * key2. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return Command Response - A String containing the longest common subsequence + * between the 2 strings. An empty String is returned if the keys do not exist or + * have no common subsequences. + */ + public T lcs(@NonNull ArgType key1, @NonNull ArgType key2) { + checkTypeOrThrow(key1); + protobufTransaction.addCommands(buildCommand(LCS, newArgsBuilder().add(key1).add(key2))); return getThis(); } /** - * Returns the server time. + * Returns the length of the longest common subsequence between strings stored at key1 + * and key2. * - * @see redis.io for details. - * @return Command Response - The current server time as a String array with two - * elements: A UNIX TIME and the amount of microseconds already elapsed in the - * current second. The returned array is in a [UNIX TIME, Microseconds already elapsed] - * format. + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return Command Response - The length of the longest common subsequence between the 2 strings. */ - public T time() { - protobufTransaction.addCommands(buildCommand(Time)); + public T lcsLen(@NonNull ArgType key1, @NonNull ArgType key2) { + checkTypeOrThrow(key1); + protobufTransaction.addCommands( + buildCommand(LCS, newArgsBuilder().add(key1).add(key2).add(LEN_VALKEY_API))); return getThis(); } /** - * Returns UNIX TIME of the last DB save timestamp or startup timestamp if no save - * was made since then. + * Publishes message on pubsub channel. * - * @see redis.io for details. - * @return Command Response - UNIX TIME of the last DB save executed with success. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param message The message to publish. + * @param channel The channel to publish the message on. + * @return Command response - The number of clients that received the message. */ - public T lastsave() { - protobufTransaction.addCommands(buildCommand(LastSave)); + public T publish(@NonNull ArgType message, @NonNull ArgType channel) { + checkTypeOrThrow(channel); + protobufTransaction.addCommands( + buildCommand(Publish, newArgsBuilder().add(channel).add(message))); return getThis(); } /** - * Returns the string representation of the type of the value stored at key. + * Gets the union of all the given sets. * - * @see valkey.io for details. + * @param keys The keys of the sets. + * @return Command Response - A Set of members which are present in at least one of + * the given sets. If none of the sets exist, an empty set will be returned. */ - public T type(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - protobufTransaction.addCommands(buildCommand(Type, commandArgs)); + public T sunion(@NonNull ArgType[] keys) { + checkTypeOrThrow(keys); + protobufTransaction.addCommands(buildCommand(SUnion, newArgsBuilder().add(keys))); return getThis(); } /** - * Inserts element in the list at key either before or after the - * pivot. + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. * - * @see redis.io for details. - * @param key The key of the list. - * @param position The relative position to insert into - either {@link InsertPosition#BEFORE} or - * {@link InsertPosition#AFTER} the pivot. - * @param pivot An element of the list. - * @param element The new element to insert. - * @return Command Response - The list length after a successful insert operation.
- * If the key doesn't exist returns -1.
- * If the pivot wasn't found, returns 0. + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return Command Response - A Map containing the indices of the longest common + * subsequence between the 2 strings and the length of the longest common subsequence. The + * resulting map contains two keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd123" and key2 + * holds the string "bcdef123" then the sample result would be + *
{@code
+     * new Long[][][] {
+     *     {
+     *         {4L, 6L},
+     *         {5L, 7L}
+     *     },
+     *     {
+     *         {1L, 3L},
+     *         {0L, 2L}
+     *     }
+     * }
+     * }
+ * The result indicates that the first substring match is "123" in key1 + * at index 4 to 6 which matches the substring in key2 + * at index 5 to 7. And the second substring match is + * "bcd" in key1 at index 1 to 3 which matches + * the substring in key2 at index 0 to 2. */ - public T linsert( - @NonNull String key, - @NonNull InsertPosition position, - @NonNull String pivot, - @NonNull String element) { - ArgsArray commandArgs = buildArgs(key, position.toString(), pivot, element); - protobufTransaction.addCommands(buildCommand(LInsert, commandArgs)); + public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2) { + checkTypeOrThrow(key1); + protobufTransaction.addCommands( + buildCommand(LCS, newArgsBuilder().add(key1).add(key2).add(IDX_COMMAND_STRING))); return getThis(); } /** - * Pops an element from the tail of the first list that is non-empty, with the given keys being - * checked in the order that they are given.
- * Blocks the connection when there are no elements to pop from any of the given lists. + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. * - * @see redis.io for details. - * @apiNote BRPOP is a client blocking command, see Blocking - * Commands for more details and best practices. - * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BRPOP operation to - * complete. A value of 0 will block indefinitely. - * @return Command Response - An array containing the key from which the - * element was popped and the value of the popped element, formatted as - * [key, value]. If no element could be popped and the timeout expired, returns - * null. + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @param minMatchLen The minimum length of matches to include in the result. + * @return Command Response - A Map containing the indices of the longest common + * subsequence between the 2 strings and the length of the longest common subsequence. The + * resulting map contains two keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd123" and key2 + * holds the string "bcdef123" then the sample result would be + *
{@code
+     * new Long[][][] {
+     *     {
+     *         {4L, 6L},
+     *         {5L, 7L}
+     *     },
+     *     {
+     *         {1L, 3L},
+     *         {0L, 2L}
+     *     }
+     * }
+     * }
+ * The result indicates that the first substring match is "123" in key1 + * at index 4 to 6 which matches the substring in key2 + * at index 5 to 7. And the second substring match is + * "bcd" in key1 at index 1 to 3 which matches + * the substring in key2 at index 0 to 2. */ - public T brpop(@NonNull String[] keys, double timeout) { - ArgsArray commandArgs = buildArgs(ArrayUtils.add(keys, Double.toString(timeout))); - protobufTransaction.addCommands(buildCommand(Brpop, commandArgs)); + public T lcsIdx(@NonNull ArgType key1, @NonNull ArgType key2, long minMatchLen) { + checkTypeOrThrow(key1); + protobufTransaction.addCommands( + buildCommand( + LCS, + newArgsBuilder() + .add(key1) + .add(key2) + .add(IDX_COMMAND_STRING) + .add(MINMATCHLEN_COMMAND_STRING) + .add(minMatchLen))); return getThis(); } /** - * Inserts specified values at the head of the list, only if key already - * exists and holds a list. + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. * - * @see redis.io for details. - * @param key The key of the list. - * @param elements The elements to insert at the head of the list stored at key. - * @return Command Response - The length of the list after the push operation. + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return Command Response - A Map containing the indices of the longest common + * subsequence between the 2 strings and the length of the longest common subsequence. The + * resulting map contains two keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd1234" and key2 + * holds the string "bcdef1234" then the sample result would be + *
{@code
+     * new Object[] {
+     *     new Object[] {
+     *         new Long[] {4L, 7L},
+     *         new Long[] {5L, 8L},
+     *         4L
+     *     },
+     *     new Object[] {
+     *         new Long[] {1L, 3L},
+     *         new Long[] {0L, 2L},
+     *         3L
+     *     }
+     * }
+     * }
+ * The result indicates that the first substring match is "1234" in key1 + * at index 4 to 7 which matches the substring in key2 + * at index 5 to 8 and the last element in the array is the + * length of the substring match which is 4. And the second substring match is + * "bcd" in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2 and + * the last element in the array is the length of the substring match which is 3. */ - public T lpushx(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - protobufTransaction.addCommands(buildCommand(LPushX, commandArgs)); + public T lcsIdxWithMatchLen(@NonNull ArgType key1, @NonNull ArgType key2) { + checkTypeOrThrow(key1); + protobufTransaction.addCommands( + buildCommand( + LCS, + newArgsBuilder() + .add(key1) + .add(key2) + .add(IDX_COMMAND_STRING) + .add(WITHMATCHLEN_COMMAND_STRING))); return getThis(); } /** - * Inserts specified values at the tail of the list, only if key already - * exists and holds a list. + * Returns the indices and length of the longest common subsequence between strings stored at + * key1 and key2. * - * @see redis.io for details. - * @param key The key of the list. - * @param elements The elements to insert at the tail of the list stored at key. - * @return Command Response - The length of the list after the push operation. + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @param minMatchLen The minimum length of matches to include in the result. + * @return Command Response - A Map containing the indices of the longest common + * subsequence between the 2 strings and the length of the longest common subsequence. The + * resulting map contains two keys, "matches" and "len": + *
    + *
  • "len" is mapped to the length of the longest common subsequence between the 2 strings + * stored as Long. + *
  • "matches" is mapped to a three dimensional Long array that stores pairs + * of indices that represent the location of the common subsequences in the strings held + * by key1 and key2. + *
+ * + * @example If key1 holds the string "abcd1234" and key2 + * holds the string "bcdef1234" then the sample result would be + *
{@code
+     * new Object[] {
+     *     new Object[] {
+     *         new Long[] { 4L, 7L },
+     *         new Long[] { 5L, 8L },
+     *         4L
+     *     },
+     *     new Object[] {
+     *         new Long[] { 1L, 3L },
+     *         new Long[] { 0L, 2L },
+     *         3L
+     *     }
+     * }
+     * }
+ * The result indicates that the first substring match is "1234" in key1 + * at index 4 to 7 which matches the substring in key2 + * at index 5 to 8 and the last element in the array is the + * length of the substring match which is 4. And the second substring match is + * "bcd" in key1 at index 1 to 3 which + * matches the substring in key2 at index 0 to 2 and + * the last element in the array is the length of the substring match which is 3. */ - public T rpushx(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - protobufTransaction.addCommands(buildCommand(RPushX, commandArgs)); + public T lcsIdxWithMatchLen( + @NonNull ArgType key1, @NonNull ArgType key2, long minMatchLen) { + checkTypeOrThrow(key1); + protobufTransaction.addCommands( + buildCommand( + LCS, + newArgsBuilder() + .add(key1) + .add(key2) + .add(IDX_COMMAND_STRING) + .add(MINMATCHLEN_COMMAND_STRING) + .add(minMatchLen) + .add(WITHMATCHLEN_COMMAND_STRING))); return getThis(); } /** - * Pops an element from the head of the first list that is non-empty, with the given keys being - * checked in the order that they are given.
- * Blocks the connection when there are no elements to pop from any of the given lists. + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(ArgType, ArgType)}. * - * @see redis.io for details. - * @apiNote BLPOP is a client blocking command, see Blocking - * Commands for more details and best practices. - * @param keys The keys of the lists to pop from. - * @param timeout The number of seconds to wait for a blocking BLPOP operation to - * complete. A value of 0 will block indefinitely. - * @return Command Response - An array containing the key from which the - * element was popped and the value of the popped element, formatted as - * [key, value]. If no element could be popped and the timeout expired, returns - * null. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An Array of sorted elements. */ - public T blpop(@NonNull String[] keys, double timeout) { - ArgsArray commandArgs = buildArgs(ArrayUtils.add(keys, Double.toString(timeout))); - protobufTransaction.addCommands(buildCommand(Blpop, commandArgs)); + public T sort(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Sort, newArgsBuilder().add(key))); return getThis(); } /** - * Returns the specified range of elements in the sorted set stored at key.
- * ZRANGE can perform different types of range queries: by index (rank), by the - * score, or by lexicographical order.
- * To get the elements with their scores, see {@link #zrangeWithScores}. + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @since Valkey 7.0 and above. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An Array of sorted elements. + */ + public T sortReadOnly(@NonNull ArgType key) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SortReadOnly, newArgsBuilder().add(key))); + return getThis(); + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(ArgType)} or {@link + * #sortReadOnly(ArgType)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public T sortStore(@NonNull ArgType key, @NonNull ArgType destination) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Sort, newArgsBuilder().add(key).add(STORE_COMMAND_STRING).add(destination))); + return getThis(); + } + + /** + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(ArgType, Map)}, which are within the borders of the area specified by a given shape. * - * @see redis.io for more details. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. - * @param rangeQuery The range query object representing the type of range query to perform.
+ * @param searchFrom The query's center point options, could be one of: *
    - *
  • For range queries by index (rank), use {@link RangeByIndex}. - *
  • For range queries by lexicographical order, use {@link RangeByLex}. - *
  • For range queries by score, use {@link RangeByScore}. + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. *
* - * @param reverse If true, reverses the sorted set, with index 0 as the element with the highest - * score. - * @return Command Response - An array of elements within the specified range. If key - * does not exist, it is treated as an empty sorted set, and the command returns an empty - * array. + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @return Command Response - An array of matched member names. */ - public T zrange(@NonNull String key, @NonNull RangeQuery rangeQuery, boolean reverse) { - ArgsArray commandArgs = buildArgs(createZRangeArgs(key, rangeQuery, reverse, false)); - protobufTransaction.addCommands(buildCommand(Zrange, commandArgs)); + public T geosearch( + @NonNull ArgType key, @NonNull SearchOrigin searchFrom, @NonNull GeoSearchShape searchBy) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + GeoSearch, newArgsBuilder().add(key).add(searchFrom.toArgs()).add(searchBy.toArgs()))); return getThis(); } /** - * Returns the specified range of elements in the sorted set stored at key.
- * ZRANGE can perform different types of range queries: by index (rank), by the - * score, or by lexicographical order.
- * To get the elements with their scores, see {@link #zrangeWithScores}. + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(ArgType, Map)}, which are within the borders of the area specified by a given shape. * - * @see redis.io for more details. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. - * @param rangeQuery The range query object representing the type of range query to perform.
+ * @param searchFrom The query's center point options, could be one of: *
    - *
  • For range queries by index (rank), use {@link RangeByIndex}. - *
  • For range queries by lexicographical order, use {@link RangeByLex}. - *
  • For range queries by score, use {@link RangeByScore}. + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. *
* - * @return Command Response - An array of elements within the specified range. If key - * does not exist, it is treated as an empty sorted set, and the command returns an empty - * array. + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return Command Response - An array of matched member names. */ - public T zrange(@NonNull String key, @NonNull RangeQuery rangeQuery) { - return getThis().zrange(key, rangeQuery, false); + public T geosearch( + @NonNull ArgType key, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchResultOptions resultOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + GeoSearch, + newArgsBuilder() + .add(key) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(resultOptions.toArgs()))); + return getThis(); } /** - * Returns the specified range of elements with their scores in the sorted set stored at key - * . Similar to {@link #zrange} but with a WITHSCORE flag. + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(ArgType, Map)}, which are within the borders of the area specified by a given shape. * - * @see redis.io for more details. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. - * @param rangeQuery The range query object representing the type of range query to perform.
+ * @param searchFrom The query's center point options, could be one of: *
    - *
  • For range queries by index (rank), use {@link RangeByIndex}. - *
  • For range queries by score, use {@link RangeByScore}. + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. *
* - * @param reverse If true, reverses the sorted set, with index 0 as the element with the highest - * score. - * @return Command Response - A Map of elements and their scores within the specified - * range. If key does not exist, it is treated as an empty sorted set, and the - * command returns an empty Map. + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @return Command Response - A 2D array of arrays where each sub-array represents a + * single item in the following order: + *
    + *
  • The member (location) name. + *
  • The distance from the center as a Double, in the same unit specified for + * searchBy. + *
  • The geohash of the location as a Long. + *
  • The coordinates as a two item array of Double. + *
*/ - public T zrangeWithScores( - @NonNull String key, @NonNull ScoredRangeQuery rangeQuery, boolean reverse) { - ArgsArray commandArgs = buildArgs(createZRangeArgs(key, rangeQuery, reverse, true)); - protobufTransaction.addCommands(buildCommand(Zrange, commandArgs)); + public T geosearch( + @NonNull ArgType key, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchOptions options) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + GeoSearch, + newArgsBuilder() + .add(key) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()))); return getThis(); } /** - * Returns the specified range of elements with their scores in the sorted set stored at key - * . Similar to {@link #zrange} but with a WITHSCORE flag. + * Returns the members of a sorted set populated with geospatial information using {@link + * #geoadd(ArgType, Map)}, which are within the borders of the area specified by a given shape. * - * @see redis.io for more details. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. * @param key The key of the sorted set. - * @param rangeQuery The range query object representing the type of range query to perform.
+ * @param searchFrom The query's center point options, could be one of: *
    - *
  • For range queries by index (rank), use {@link RangeByIndex}. - *
  • For range queries by score, use {@link RangeByScore}. + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. *
* - * @return Command Response - A Map of elements and their scores within the specified - * range. If key does not exist, it is treated as an empty sorted set, and the - * command returns an empty Map. + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return Command Response - A 2D array of arrays where each sub-array represents a + * single item in the following order: + *
    + *
  • The member (location) name. + *
  • The distance from the center as a Double, in the same unit specified for + * searchBy. + *
  • The geohash of the location as a Long. + *
  • The coordinates as a two item array of Double. + *
*/ - public T zrangeWithScores(@NonNull String key, @NonNull ScoredRangeQuery rangeQuery) { - return getThis().zrangeWithScores(key, rangeQuery, false); + public T geosearch( + @NonNull ArgType key, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchOptions options, + @NonNull GeoSearchResultOptions resultOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + GeoSearch, + newArgsBuilder() + .add(key) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()) + .add(resultOptions.toArgs()))); + return getThis(); } /** - * Adds all elements to the HyperLogLog data structure stored at the specified key. - *
- * Creates a new structure if the key does not exist. + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(ArgType, SearchOrigin, + * GeoSearchShape)}. * - *

When no elements are provided, and key exists and is a - * HyperLogLog, then no operation is performed. If key does not exist, then the - * HyperLogLog structure is created. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *

    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
* - * @see redis.io for details. - * @param key The key of the HyperLogLog data structure to add elements into. - * @param elements An array of members to add to the HyperLogLog stored at key. - * @return Command Response - If the HyperLogLog is newly created, or if the HyperLogLog - * approximated cardinality is altered, then returns 1. Otherwise, returns - * 0. + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @return Command Response - The number of elements in the resulting set. */ - public T pfadd(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - protobufTransaction.addCommands(buildCommand(PfAdd, commandArgs)); + public T geosearchstore( + @NonNull ArgType destination, + @NonNull ArgType source, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand( + GeoSearchStore, + newArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()))); return getThis(); } /** - * Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or - * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(ArgType, SearchOrigin, + * GeoSearchShape, GeoSearchResultOptions)}. * - * @see redis.io for details. - * @param keys The keys of the HyperLogLog data structures to be analyzed. - * @return Command Response - The approximated cardinality of given HyperLogLog data structures. - *
- * The cardinality of a key that does not exist is 0. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return Command Response - The number of elements in the resulting set. */ - public T pfcount(@NonNull String[] keys) { - ArgsArray commandArgs = buildArgs(keys); - protobufTransaction.addCommands(buildCommand(PfCount, commandArgs)); + public T geosearchstore( + @NonNull ArgType destination, + @NonNull ArgType source, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchResultOptions resultOptions) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand( + GeoSearchStore, + newArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(resultOptions.toArgs()))); return getThis(); } /** - * Merges multiple HyperLogLog values into a unique value.
- * If the destination variable exists, it is treated as one of the source HyperLogLog data sets, - * otherwise a new HyperLogLog is created. + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(ArgType, SearchOrigin, + * GeoSearchShape, GeoSearchOptions)}. * - * @see redis.io for details. - * @param destination The key of the destination HyperLogLog where the merged data sets will be - * stored. - * @param sourceKeys The keys of the HyperLogLog structures to be merged. - * @return Command Response - OK. + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @return Command Response - The number of elements in the resulting set. + */ + public T geosearchstore( + @NonNull ArgType destination, + @NonNull ArgType source, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchStoreOptions options) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand( + GeoSearchStore, + newArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()))); + return getThis(); + } + + /** + * Searches for members in a sorted set stored at source representing geospatial data + * within a circular or rectangular area and stores the result in destination. If + * destination already exists, it is overwritten. Otherwise, a new sorted set will be + * created. To get the result directly, see `{@link #geosearch(ArgType, SearchOrigin, + * GeoSearchShape, GeoSearchOptions, GeoSearchResultOptions)}. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param destination The key of the destination sorted set. + * @param source The key of the source sorted set. + * @param searchFrom The query's center point options, could be one of: + *
    + *
  • {@link MemberOrigin} to use the position of the given existing member in the sorted + * set. + *
  • {@link CoordOrigin} to use the given longitude and latitude coordinates. + *
+ * + * @param searchBy The query's shape options: + *
    + *
  • {@link GeoSearchShape#GeoSearchShape(double, GeoUnit)} to search inside circular area + * according to given radius. + *
  • {@link GeoSearchShape#GeoSearchShape(double, double, GeoUnit)} to search inside an + * axis-aligned rectangle, determined by height and width. + *
+ * + * @param options The optional inputs to request additional information. + * @param resultOptions Optional inputs for sorting/limiting the results. See - {@link + * GeoSearchResultOptions} + * @return Command Response - The number of elements in the resulting set. */ - public T pfmerge(@NonNull String destination, @NonNull String[] sourceKeys) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sourceKeys, destination)); - protobufTransaction.addCommands(buildCommand(PfMerge, commandArgs)); + public T geosearchstore( + @NonNull ArgType destination, + @NonNull ArgType source, + @NonNull SearchOrigin searchFrom, + @NonNull GeoSearchShape searchBy, + @NonNull GeoSearchStoreOptions options, + @NonNull GeoSearchResultOptions resultOptions) { + checkTypeOrThrow(destination); + protobufTransaction.addCommands( + buildCommand( + GeoSearchStore, + newArgsBuilder() + .add(destination) + .add(source) + .add(searchFrom.toArgs()) + .add(searchBy.toArgs()) + .add(options.toArgs()) + .add(resultOptions.toArgs()))); + return getThis(); + } + + /** + * Iterates incrementally over a set. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the set. The second element is + * always an Array of the subset of the set held in key. + */ + public T sscan(@NonNull ArgType key, @NonNull ArgType cursor) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(SScan, newArgsBuilder().add(key).add(cursor))); + return getThis(); + } + + /** + * Iterates incrementally over a set. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param sScanOptions The {@link SScanOptions}. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the set. The second element is + * always an Array of the subset of the set held in key. + */ + public T sscan( + @NonNull ArgType key, @NonNull ArgType cursor, @NonNull SScanOptions sScanOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SScan, newArgsBuilder().add(key).add(cursor).add(sScanOptions.toArgs()))); + return getThis(); + } + + /** + * Iterates incrementally over a sorted set. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the sorted set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the sorted set. The second + * element is always an Array of the subset of the sorted set held in key + * . The array in the second element is always a flattened series of String + * pairs, where the value is at even indices and the score is at odd indices. + */ + public T zscan(@NonNull ArgType key, @NonNull ArgType cursor) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(ZScan, newArgsBuilder().add(key).add(cursor))); + return getThis(); + } + + /** + * Iterates incrementally over a sorted set. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the sorted set. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param zScanOptions The {@link ZScanOptions}. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the sorted set. The second + * element is always an Array of the subset of the sorted set held in key + * . The array in the second element is always a flattened series of String + * pairs, where the value is at even indices and the score is at odd indices. + */ + public T zscan( + @NonNull ArgType key, @NonNull ArgType cursor, @NonNull ZScanOptions zScanOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(ZScan, newArgsBuilder().add(key).add(cursor).add(zScanOptions.toArgs()))); + return getThis(); + } + + /** + * Iterates fields of Hash types and their associated values. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the hash. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the result. The second element is + * always an Array of the subset of the hash held in key. The array + * in the second element is always a flattened series of String pairs, where the + * key is at even indices and the value is at odd indices. + */ + public T hscan(@NonNull ArgType key, @NonNull ArgType cursor) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(HScan, newArgsBuilder().add(key).add(cursor))); + return getThis(); + } + + /** + * Iterates fields of Hash types and their associated values. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the hash. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param hScanOptions The {@link HScanOptions}. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the result. The second element is + * always an Array of the subset of the hash held in key. The array + * in the second element is always a flattened series of String pairs, where the + * key is at even indices and the value is at odd indices. + */ + public T hscan( + @NonNull ArgType key, @NonNull ArgType cursor, @NonNull HScanOptions hScanOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(HScan, newArgsBuilder().add(key).add(cursor).add(hScanOptions.toArgs()))); + return getThis(); + } + + /** + * Returns the number of replicas that acknowledged the write commands sent by the current client + * before this command, both in the case where the specified number of replicas are reached, or + * when the timeout is reached. + * + * @see valkey.io for details. + * @param numReplicas The number of replicas to reach. + * @param timeout The timeout value specified in milliseconds. + * @return Command Response - The number of replicas reached by all the writes performed in the + * context of the current connection. + */ + public T wait(long numReplicas, long timeout) { + protobufTransaction.addCommands( + buildCommand(Wait, newArgsBuilder().add(numReplicas).add(timeout))); return getThis(); } /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { - return buildCommand(requestType, buildArgs()); + return buildCommand(requestType, emptyArgs()); } /** Build protobuf {@link Command} object for given command and arguments. */ @@ -2234,14 +7103,52 @@ protected Command buildCommand(RequestType requestType, ArgsArray args) { return Command.newBuilder().setRequestType(requestType).setArgsArray(args).build(); } - /** Build protobuf {@link ArgsArray} object for given arguments. */ - protected ArgsArray buildArgs(String... stringArgs) { + /** Build protobuf {@link Command} object for given command and arguments. */ + protected Command buildCommand(RequestType requestType, ArgsBuilder argsBuilder) { + final Command.Builder builder = Command.newBuilder(); + builder.setRequestType(requestType); + CommandManager.populateCommandWithArgs(argsBuilder.toArray(), builder); + return builder.build(); + } + + /** Build protobuf {@link ArgsArray} object for empty arguments. */ + protected ArgsArray emptyArgs() { ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); + return commandArgs.build(); + } + + protected ArgsBuilder newArgsBuilder() { + return new ArgsBuilder(); + } + + protected void checkTypeOrThrow(ArgType arg) { + if ((arg instanceof String) || (arg instanceof GlideString)) { + return; + } + throw new IllegalArgumentException("Expected String or GlideString"); + } - for (String string : stringArgs) { - commandArgs.addArgs(string); + protected void checkTypeOrThrow(ArgType[] args) { + if (args.length == 0) { + // nothing to check here + return; } + checkTypeOrThrow(args[0]); + } - return commandArgs.build(); + protected void checkTypeOrThrow(Map argsMap) { + if (argsMap.isEmpty()) { + // nothing to check here + return; + } + + var arg = argsMap.keySet().iterator().next(); + checkTypeOrThrow(arg); + } + + /** Helper function for creating generic type ("ArgType") array */ + @SafeVarargs + protected final ArgType[] createArray(ArgType... args) { + return args; } } diff --git a/java/client/src/main/java/glide/api/models/ClusterTransaction.java b/java/client/src/main/java/glide/api/models/ClusterTransaction.java index e2c4820057..be02a787be 100644 --- a/java/client/src/main/java/glide/api/models/ClusterTransaction.java +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -1,30 +1,139 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; -import lombok.AllArgsConstructor; +import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; + +import glide.api.GlideClusterClient; +import glide.api.models.commands.SortClusterOptions; +import lombok.NonNull; /** - * Extends BaseTransaction class for cluster mode commands. Transactions allow the execution of a - * group of commands in a single step. + * Transaction implementation for cluster {@link GlideClusterClient}. Transactions allow the + * execution of a group of commands in a single step. * - *

Command Response: An array of command responses is returned by the client exec - * command, in the order they were given. Each element in the array represents a command given to - * the Transaction. The response for each command depends on the executed Redis - * command. Specific response types are documented alongside each method. + *

Transaction Response: An array of command responses is returned by the client + * {@link GlideClusterClient#exec} command, in the order they were given. Each element in the array + * represents a command given to the {@link ClusterTransaction}. The response for each command + * depends on the executed command. Specific response types are documented alongside each method. * * @example - *

- *  ClusterTransaction transaction = new ClusterTransaction();
- *    .set("key", "value");
- *    .get("key");
- *  ClusterValue[] result = client.exec(transaction, route).get();
- *  // result contains: OK and "value"
- *  
+ *
{@code
+ * ClusterTransaction transaction = new ClusterTransaction();
+ *   .set("key", "value");
+ *   .get("key");
+ * Object[] result = client.exec(transaction).get();
+ * // result contains: OK and "value"
+ * }
*/ -@AllArgsConstructor public class ClusterTransaction extends BaseTransaction { + @Override protected ClusterTransaction getThis() { return this; } + + /** + * Publishes message on pubsub channel in sharded mode. + * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param message The message to publish. + * @param channel The channel to publish the message on. + * @param sharded Indicates that this should be run in sharded mode. Setting sharded + * to true is only applicable with Valkey 7.0+. + * @return Command response - The number of clients that received the message. + */ + public ClusterTransaction publish( + @NonNull ArgType message, @NonNull ArgType channel, boolean sharded) { + if (!sharded) { + return super.publish(message, channel); + } + checkTypeOrThrow(channel); + protobufTransaction.addCommands( + buildCommand(SPublish, newArgsBuilder().add(channel).add(message))); + return getThis(); + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(ArgType, ArgType, + * SortClusterOptions)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sort( + @NonNull ArgType key, @NonNull SortClusterOptions sortClusterOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Sort, newArgsBuilder().add(key).add(sortClusterOptions.toArgs()))); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sortReadOnly( + @NonNull ArgType key, @NonNull SortClusterOptions sortClusterOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SortReadOnly, newArgsBuilder().add(key).add(sortClusterOptions.toArgs()))); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(ArgType, + * SortClusterOptions)} or {@link #sortReadOnly(ArgType, SortClusterOptions)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public ClusterTransaction sortStore( + @NonNull ArgType key, + @NonNull ArgType destination, + @NonNull SortClusterOptions sortClusterOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + Sort, + newArgsBuilder() + .add(key) + .add(sortClusterOptions.toArgs()) + .add(STORE_COMMAND_STRING) + .add(destination))); + return this; + } } diff --git a/java/client/src/main/java/glide/api/models/ClusterValue.java b/java/client/src/main/java/glide/api/models/ClusterValue.java index 360b2bcaa9..d141c3cd08 100644 --- a/java/client/src/main/java/glide/api/models/ClusterValue.java +++ b/java/client/src/main/java/glide/api/models/ClusterValue.java @@ -1,20 +1,21 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; +import java.util.stream.Collectors; /** - * Represents a returned value object from a Redis server with cluster-mode enabled. The response - * type may depend on the submitted {@link Route}. + * Represents a returned value object from a the server with cluster-mode enabled. The response type + * may depend on the submitted {@link Route}. * * @remarks ClusterValue stores values in a union-like object. It contains a single-value or - * multi-value response from Redis. If the command's routing is to a single node use {@link + * multi-value response from the server. If the command's routing is to a single node use {@link * #getSingleValue()} to return a response of type T. Otherwise, use {@link * #getMultiValue()} to return a Map of address: nodeResponse where * address is of type string and nodeResponse is of type * T. - * @see Redis cluster specification + * @see Valkey cluster specification * @param The wrapped response type */ public class ClusterValue { @@ -68,6 +69,17 @@ public static ClusterValue ofMultiValue(Map data) { return res; } + /** A constructor for the value. */ + public static ClusterValue ofMultiValueBinary(Map data) { + var res = new ClusterValue(); + // the map node address can be converted to a string + Map multiValue = + data.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); + res.multiValue = multiValue; + return res; + } + /** * Check that multi-value is stored in this object. Should be called prior to {@link * #getMultiValue()}. diff --git a/java/client/src/main/java/glide/api/models/GlideString.java b/java/client/src/main/java/glide/api/models/GlideString.java new file mode 100644 index 0000000000..a81e281c28 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/GlideString.java @@ -0,0 +1,153 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.Getter; + +/** + * Represents a Valkey string type. Since Valkey stores strings as byte[], such strings + * can contain non-UTF8 compatible symbols or even arbitrary binary data BLOBs.
+ * This class stores data byte[] too, but provides API to represent data as a {@link + * String} if conversion is possible. + * + * @see valkey.io for more details. + */ +public class GlideString implements Comparable { + + /** The Valkey string as a binary representation. */ + @Getter private byte[] bytes; + + /** + * Stores a string when it is possible.
+ * {@link String} representation of the value is only stored if conversion via {@link + * #canConvertToString()} is possible. The conversion is lazy, and only converted on the first + * call {@link #toString()}, {@link #getString()}, or {@link #canConvertToString()}. + */ + private String string = null; + + /** Flag whether possibility to convert to string was checked. */ + private final AtomicBoolean conversionChecked = new AtomicBoolean(false); + + /** Constructor is private - use {@link #gs} or {@link #of} to instantiate an object. */ + private GlideString() {} + + /** Create a GlideString using a {@link String}. */ + public static GlideString of(String string) { + var res = new GlideString(); + res.string = string; + res.bytes = string.getBytes(StandardCharsets.UTF_8); + return res; + } + + /** Create a GlideString using a byte array. */ + public static GlideString of(byte[] bytes) { + var res = new GlideString(); + res.bytes = bytes; + return res; + } + + /** Allow converting any type to GlideString */ + public static GlideString of(ArgType o) { + if (o instanceof GlideString) { + return (GlideString) o; + } else if (o instanceof byte[]) { + return GlideString.of((byte[]) o); + } else if (o instanceof String) { + return GlideString.of((String) o); + } else { + var res = new GlideString(); + res.string = o.toString(); + res.bytes = res.string.getBytes(StandardCharsets.UTF_8); + return res; + } + } + + /** Create a GlideString using a {@link String}. */ + public static GlideString gs(String string) { + return GlideString.of(string); + } + + /** Create a GlideString using a byte array. */ + public static GlideString gs(byte[] bytes) { + return GlideString.of(bytes); + } + + /** Converts stored data to a human-friendly {@link String} if it is possible. */ + @Override + public String toString() { + return getString(); + } + + /** Converts stored data to a human-friendly {@link String} if it is possible. */ + public String getString() { + if (string != null) { + return string; + } + + if (canConvertToString()) { + return string; + } + return String.format("Value not convertible to string: byte[] %d", Arrays.hashCode(bytes)); + } + + /** Compare with another GlideString. */ + public int compareTo(GlideString o) { + return Arrays.compare(this.bytes, o.bytes); + } + + /** Check whether stored data could be converted to a {@link String}. */ + public boolean canConvertToString() { + if (string != null) { + return true; + } + + // double-checked locking + if (conversionChecked.get()) { + return false; + } else { + synchronized (this) { + if (conversionChecked.get()) { + return false; + } else { + try { + // TODO find a better way to check this + // Detect whether `bytes` could be represented by a `String` without data corruption + var tmpStr = new String(bytes, StandardCharsets.UTF_8); + if (Arrays.equals(bytes, tmpStr.getBytes(StandardCharsets.UTF_8))) { + string = tmpStr; + return true; + } else { + return false; + } + } finally { + conversionChecked.set(true); + } + } + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GlideString)) return false; + GlideString that = (GlideString) o; + + return Arrays.equals(bytes, that.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + /** Method to concatenate two GlideString objects */ + public GlideString concat(GlideString other) { + byte[] concatenatedBytes = new byte[this.bytes.length + other.bytes.length]; + System.arraycopy(this.bytes, 0, concatenatedBytes, 0, this.bytes.length); + System.arraycopy(other.bytes, 0, concatenatedBytes, this.bytes.length, other.bytes.length); + return GlideString.of(concatenatedBytes); + } +} diff --git a/java/client/src/main/java/glide/api/models/PubSubMessage.java b/java/client/src/main/java/glide/api/models/PubSubMessage.java new file mode 100644 index 0000000000..d96754e72e --- /dev/null +++ b/java/client/src/main/java/glide/api/models/PubSubMessage.java @@ -0,0 +1,41 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models; + +import java.util.Optional; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** PubSub message received by the client. */ +@Getter +@EqualsAndHashCode +public class PubSubMessage { + /** An incoming message received. */ + private final GlideString message; + + /** A name of the originating channel. */ + private final GlideString channel; + + /** A pattern matched to the channel name. */ + private final Optional pattern; + + public PubSubMessage(GlideString message, GlideString channel, GlideString pattern) { + this.message = message; + this.channel = channel; + this.pattern = Optional.ofNullable(pattern); + } + + public PubSubMessage(GlideString message, GlideString channel) { + this.message = message; + this.channel = channel; + this.pattern = Optional.empty(); + } + + @Override + public String toString() { + String res = String.format("(%s, channel = %s", message, channel); + if (pattern.isPresent()) { + res += ", pattern = " + pattern.get(); + } + return res + ")"; + } +} diff --git a/java/client/src/main/java/glide/api/models/Script.java b/java/client/src/main/java/glide/api/models/Script.java index 80688ddd2f..6a074e2573 100644 --- a/java/client/src/main/java/glide/api/models/Script.java +++ b/java/client/src/main/java/glide/api/models/Script.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; import static glide.ffi.resolvers.ScriptResolver.dropScript; @@ -18,19 +18,30 @@ public class Script implements AutoCloseable { /** Hash string representing the code. */ @Getter private final String hash; + private boolean isDropped = false; + + /** Indication if script invocation output can return binary data. */ + @Getter private final Boolean binaryOutput; + /** * Wraps around creating a Script object from code. * * @param code To execute with a ScriptInvoke call. + * @param binaryOutput Indicates if the output can return binary data. */ - public Script(String code) { - hash = storeScript(code); + public Script(T code, Boolean binaryOutput) { + this.hash = storeScript(GlideString.of(code).getBytes()); + this.binaryOutput = binaryOutput; } /** Drop the linked script from glide-rs code. */ @Override public void close() throws Exception { - dropScript(hash); + + if (!isDropped) { + dropScript(hash); + isDropped = true; + } } @Override diff --git a/java/client/src/main/java/glide/api/models/Transaction.java b/java/client/src/main/java/glide/api/models/Transaction.java index 6f9aa49005..9e691d87d5 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -1,19 +1,29 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; -import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.Move; +import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.Select; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; +import static glide.api.commands.GenericCommands.DB_VALKEY_API; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; -import lombok.AllArgsConstructor; -import redis_request.RedisRequestOuterClass; +import glide.api.GlideClient; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.scan.ScanOptions; +import lombok.NonNull; /** - * Extends BaseTransaction class for Redis standalone commands. Transactions allow the execution of - * a group of commands in a single step. + * Transaction implementation for standalone {@link GlideClient}. Transactions allow the execution + * of a group of commands in a single step. * - *

Command Response: An array of command responses is returned by the client exec - * command, in the order they were given. Each element in the array represents a command given to - * the Transaction. The response for each command depends on the executed Redis - * command. Specific response types are documented alongside each method. + *

Transaction Response: An array of command responses is returned by the client + * {@link GlideClient#exec} API, in the order they were given. Each element in the array represents + * a command given to the {@link Transaction}. The response for each command depends on the executed + * Valkey command. Specific response types are documented alongside each method. * * @example *

{@code
@@ -26,24 +36,199 @@
  * assert result[1].equals("value");
  * }
*/ -@AllArgsConstructor public class Transaction extends BaseTransaction { + @Override protected Transaction getThis() { return this; } /** - * Changes the currently selected Redis database. + * Changes the currently selected server database. * - * @see redis.io for details. + * @see valkey.io for details. * @param index The index of the database to select. * @return Command Response - A simple OK response. */ public Transaction select(long index) { - RedisRequestOuterClass.Command.ArgsArray commandArgs = buildArgs(Long.toString(index)); + protobufTransaction.addCommands(buildCommand(Select, newArgsBuilder().add(index))); + return this; + } + + /** + * Move key from the currently selected database to the database specified by + * dbIndex. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for more details. + * @param key The key to move. + * @param dbIndex The index of the database to move key to. + * @return Command Response - true if key was moved, or false + * if the key already exists in the destination database or does not + * exist in the source database. + */ + public Transaction move(ArgType key, long dbIndex) { + checkTypeOrThrow(key); + protobufTransaction.addCommands(buildCommand(Move, newArgsBuilder().add(key).add(dbIndex))); + return this; + } + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public Transaction copy( + @NonNull ArgType source, @NonNull ArgType destination, long destinationDB) { + return copy(source, destination, destinationDB, false); + } + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Valkey 6.2.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @param replace If the destination key should be removed before copying the value to it. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public Transaction copy( + @NonNull ArgType source, @NonNull ArgType destination, long destinationDB, boolean replace) { + checkTypeOrThrow(source); + protobufTransaction.addCommands( + buildCommand( + Copy, + newArgsBuilder() + .add(source) + .add(destination) + .add(DB_VALKEY_API) + .add(destinationDB) + .addIf(REPLACE_VALKEY_API, replace))); + return this; + } - protobufTransaction.addCommands(buildCommand(Select, commandArgs)); + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(ArgType, ArgType, SortOptions)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sort(@NonNull ArgType key, @NonNull SortOptions sortOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(Sort, newArgsBuilder().add(key).add(sortOptions.toArgs()))); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * + * @since Valkey 7.0 and above. + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sortReadOnly( + @NonNull ArgType key, @NonNull SortOptions sortOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand(SortReadOnly, newArgsBuilder().add(key).add(sortOptions.toArgs()))); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(ArgType, SortOptions)}. + * + * @implNote {@link ArgType} is limited to {@link String} or {@link GlideString}, any other type + * will throw {@link IllegalArgumentException}. + * @see valkey.io for details. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public Transaction sortStore( + @NonNull ArgType key, @NonNull ArgType destination, @NonNull SortOptions sortOptions) { + checkTypeOrThrow(key); + protobufTransaction.addCommands( + buildCommand( + Sort, + newArgsBuilder() + .add(key) + .add(sortOptions.toArgs()) + .add(STORE_COMMAND_STRING) + .add(destination))); + return this; + } + + /** + * Iterates incrementally over a database for matching keys. + * + * @see valkey.io for details. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the scan.
+ * The second element is always an Array of matched keys from the database. + */ + public Transaction scan(@NonNull ArgType cursor) { + checkTypeOrThrow(cursor); + protobufTransaction.addCommands(buildCommand(Scan, newArgsBuilder().add(cursor))); + return this; + } + + /** + * Iterates incrementally over a database for matching keys. + * + * @see valkey.io for details. + * @param cursor The cursor that points to the next iteration of results. A value of "0" + * indicates the start of the search. + * @param options The {@link ScanOptions}. + * @return Command Response - An Array of Objects. The first element is + * always the cursor for the next iteration of results. "0" will be + * the cursor returned on the last iteration of the scan.
+ * The second element is always an Array of matched keys from the database. + */ + public Transaction scan(@NonNull ArgType cursor, @NonNull ScanOptions options) { + checkTypeOrThrow(cursor); + protobufTransaction.addCommands( + buildCommand(Scan, newArgsBuilder().add(cursor).add(options.toArgs()))); return this; } } diff --git a/java/client/src/main/java/glide/api/models/commands/ConditionalChange.java b/java/client/src/main/java/glide/api/models/commands/ConditionalChange.java new file mode 100644 index 0000000000..6376be4a0e --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ConditionalChange.java @@ -0,0 +1,29 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import glide.api.models.commands.geospatial.GeoAddOptions; +import java.util.Map; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * An optional condition to the {@link GeospatialIndicesBaseCommands#geoadd(String, Map, + * GeoAddOptions)} command. + */ +@RequiredArgsConstructor +@Getter +public enum ConditionalChange { + /** + * Only update elements that already exist. Don't add new elements. Equivalent to XX + * in the Valkey API. + */ + ONLY_IF_EXISTS("XX"), + /** + * Only add new elements. Don't update already existing elements. Equivalent to NX in + * the Valkey API. + */ + ONLY_IF_DOES_NOT_EXIST("NX"); + + private final String valkeyApi; +} diff --git a/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java b/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java index 2f51745af5..7847291627 100644 --- a/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; import glide.api.commands.GenericBaseCommands; @@ -8,31 +8,31 @@ * Optional arguments for {@link GenericBaseCommands#expire(String, long, ExpireOptions)}, and * similar commands. * - * @see redis.io + * @see valkey.io */ @RequiredArgsConstructor public enum ExpireOptions { /** - * Sets expiry only when the key has no expiry. Equivalent to NX in the Redis API. + * Sets expiry only when the key has no expiry. Equivalent to NX in the Valkey API. */ HAS_NO_EXPIRY("NX"), /** * Sets expiry only when the key has an existing expiry. Equivalent to XX in the - * Redis API. + * Valkey API. */ HAS_EXISTING_EXPIRY("XX"), /** * Sets expiry only when the new expiry is greater than current one. Equivalent to GT - * in the Redis API. + * in the Valkey API. */ NEW_EXPIRY_GREATER_THAN_CURRENT("GT"), /** * Sets expiry only when the new expiry is less than current one. Equivalent to LT in - * the Redis API. + * the Valkey API. */ NEW_EXPIRY_LESS_THAN_CURRENT("LT"); - private final String redisApi; + private final String valkeyApi; /** * Converts ExpireOptions into a String[]. @@ -40,6 +40,6 @@ public enum ExpireOptions { * @return String[] */ public String[] toArgs() { - return new String[] {this.redisApi}; + return new String[] {this.valkeyApi}; } } diff --git a/java/client/src/main/java/glide/api/models/commands/FlushMode.java b/java/client/src/main/java/glide/api/models/commands/FlushMode.java new file mode 100644 index 0000000000..ff7ea4c812 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/FlushMode.java @@ -0,0 +1,38 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.GlideClient; +import glide.api.GlideClusterClient; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; + +// TODO add links to script flush +/** + * Defines flushing mode for: + * + *
    + *
  • FLUSHALL command implemented by {@link GlideClient#flushall(FlushMode)}, + * {@link GlideClusterClient#flushall(FlushMode)}, and {@link + * GlideClusterClient#flushall(FlushMode, SingleNodeRoute)}. + *
  • FLUSHDB command implemented by {@link GlideClient#flushdb(FlushMode)}, {@link + * GlideClusterClient#flushdb(FlushMode)}, and {@link GlideClusterClient#flushdb(FlushMode, + * SingleNodeRoute)}. + *
  • FUNCTION FLUSH command implemented by {@link + * GlideClient#functionFlush(FlushMode)}, {@link GlideClusterClient#functionFlush(FlushMode)}, + * and {@link GlideClusterClient#functionFlush(FlushMode, Route)}. + *
+ * + * @see flushall, flushdb, and function flush at valkey.io + */ +public enum FlushMode { + /** + * Flushes synchronously. + * + * @since Valkey 6.2 and above. + */ + SYNC, + /** Flushes asynchronously. */ + ASYNC +} diff --git a/java/client/src/main/java/glide/api/models/commands/GetExOptions.java b/java/client/src/main/java/glide/api/models/commands/GetExOptions.java new file mode 100644 index 0000000000..607f2dd130 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/GetExOptions.java @@ -0,0 +1,122 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import static glide.api.models.commands.GetExOptions.ExpiryType.MILLISECONDS; +import static glide.api.models.commands.GetExOptions.ExpiryType.PERSIST; +import static glide.api.models.commands.GetExOptions.ExpiryType.SECONDS; +import static glide.api.models.commands.GetExOptions.ExpiryType.UNIX_MILLISECONDS; +import static glide.api.models.commands.GetExOptions.ExpiryType.UNIX_SECONDS; + +import glide.api.commands.StringBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; + +/** + * Optional arguments to {@link StringBaseCommands#getex(String, GetExOptions)} command. + * + * @see valkey.io + */ +public class GetExOptions { + + /** Expiry type for the time to live */ + private final ExpiryType type; + + /** The amount of time to live before the key expires. */ + private Long count; + + private GetExOptions(ExpiryType type) { + this.type = type; + } + + private GetExOptions(ExpiryType type, Long count) { + this.type = type; + this.count = count; + } + + /** + * Set the specified expire time, in seconds. Equivalent to EX in the Valkey API. + * + * @param seconds The time to expire, in seconds. + * @return The options specifying the given expiry. + */ + public static GetExOptions Seconds(Long seconds) { + return new GetExOptions(SECONDS, seconds); + } + + /** + * Set the specified expire time, in milliseconds. Equivalent to PX in the Valkey + * API. + * + * @param milliseconds The time to expire, in milliseconds. + * @return The options specifying the given expiry. + */ + public static GetExOptions Milliseconds(Long milliseconds) { + return new GetExOptions(MILLISECONDS, milliseconds); + } + + /** + * Set the specified Unix time at which the key will expire, in seconds. Equivalent to + * EXAT in the Valkey API. + * + * @param unixSeconds The UNIX TIME to expire, in seconds. + * @return The options specifying the given expiry. + */ + public static GetExOptions UnixSeconds(Long unixSeconds) { + return new GetExOptions(UNIX_SECONDS, unixSeconds); + } + + /** + * Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to + * PXAT in the Valkey API. + * + * @param unixMilliseconds The UNIX TIME to expire, in milliseconds. + * @return The options specifying the given expiry. + */ + public static GetExOptions UnixMilliseconds(Long unixMilliseconds) { + return new GetExOptions(UNIX_MILLISECONDS, unixMilliseconds); + } + + /** Remove the time to live associated with the key. */ + public static GetExOptions Persist() { + return new GetExOptions(PERSIST); + } + + /** Types of value expiration configuration. */ + @RequiredArgsConstructor + protected enum ExpiryType { + SECONDS("EX"), + MILLISECONDS("PX"), + UNIX_SECONDS("EXAT"), + UNIX_MILLISECONDS("PXAT"), + PERSIST("PERSIST"); + + private final String valkeyApi; + } + + /** + * Converts GetExOptions into a String[] to pass to the GETEX command. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + optionArgs.add(type.valkeyApi); + if (count != null) { + optionArgs.add(String.valueOf(count)); + } + return optionArgs.toArray(new String[0]); + } + + /** + * Converts GetExOptions into a GlideString[] to pass to the GETEX command. + * + * @return GlideString[] + */ + public GlideString[] toGlideStringArgs() { + return Arrays.stream(toArgs()).map(GlideString::gs).toArray(GlideString[]::new); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/InfoOptions.java b/java/client/src/main/java/glide/api/models/commands/InfoOptions.java index 8b518ab87b..081e11b7f8 100644 --- a/java/client/src/main/java/glide/api/models/commands/InfoOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/InfoOptions.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; import glide.api.commands.ServerManagementCommands; @@ -9,7 +9,7 @@ /** * Optional arguments to {@link ServerManagementCommands#info(InfoOptions)} * - * @see redis.io + * @see valkey.io */ @Builder public final class InfoOptions { @@ -17,7 +17,7 @@ public final class InfoOptions { @Singular private final List
sections; public enum Section { - /** SERVER: General information about the Redis server */ + /** SERVER: General information about the server */ SERVER, /** CLIENTS: Client connections section */ CLIENTS, @@ -31,19 +31,19 @@ public enum Section { REPLICATION, /** CPU: CPU consumption statistics */ CPU, - /** COMMANDSTATS: Redis command statistics */ + /** COMMANDSTATS: Valkey command statistics */ COMMANDSTATS, - /** LATENCYSTATS: Redis command latency percentile distribution statistics */ + /** LATENCYSTATS: Valkey command latency percentile distribution statistics */ LATENCYSTATS, - /** SENTINEL: Redis Sentinel section (only applicable to Sentinel instances) */ + /** SENTINEL: Valkey Sentinel section (only applicable to Sentinel instances) */ SENTINEL, - /** CLUSTER: Redis Cluster section */ + /** CLUSTER: Valkey Cluster section */ CLUSTER, /** MODULES: Modules section */ MODULES, /** KEYSPACE: Database related statistics */ KEYSPACE, - /** ERRORSTATS: Redis error statistics */ + /** ERRORSTATS: Valkey error statistics */ ERRORSTATS, /** ALL: Return all sections (excluding module generated ones) */ ALL, @@ -54,7 +54,7 @@ public enum Section { } /** - * Converts options enum into a String[] to add to a Redis request. + * Converts options enum into a String[] to add to the command request. * * @return String[] */ diff --git a/java/client/src/main/java/glide/api/models/commands/LInsertOptions.java b/java/client/src/main/java/glide/api/models/commands/LInsertOptions.java index 92ea5488ef..887153967c 100644 --- a/java/client/src/main/java/glide/api/models/commands/LInsertOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/LInsertOptions.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; import glide.api.commands.ListBaseCommands; @@ -6,7 +6,7 @@ /** * Options for {@link ListBaseCommands#linsert}. * - * @see redis.io + * @see valkey.io */ public class LInsertOptions { /** Defines where to insert new elements into a list. */ diff --git a/java/client/src/main/java/glide/api/models/commands/LPosOptions.java b/java/client/src/main/java/glide/api/models/commands/LPosOptions.java new file mode 100644 index 0000000000..73b4cb3018 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/LPosOptions.java @@ -0,0 +1,52 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.ListBaseCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +/** + * Optional arguments to {@link ListBaseCommands#lpos(String, String, LPosOptions)} and {@link + * ListBaseCommands#lposCount(String, String, long)} command. + * + * @see valkey.io + */ +@Builder +public final class LPosOptions { + + /** The rank of the match to return. */ + private Long rank; + + /** The maximum number of comparisons to make between the element and the items in the list. */ + private Long maxLength; + + /** Valkey API keyword used to extract specific number of matching indices from a list. */ + public static final String COUNT_VALKEY_API = "COUNT"; + + /** Valkey API keyword use to determine the rank of the match to return. */ + public static final String RANK_VALKEY_API = "RANK"; + + /** Valkey API keyword used to determine the maximum number of list items to compare. */ + public static final String MAXLEN_VALKEY_API = "MAXLEN"; + + /** + * Converts LPosOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + if (rank != null) { + optionArgs.add(RANK_VALKEY_API); + optionArgs.add(String.valueOf(rank)); + } + + if (maxLength != null) { + optionArgs.add(MAXLEN_VALKEY_API); + optionArgs.add(String.valueOf(maxLength)); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/ListDirection.java b/java/client/src/main/java/glide/api/models/commands/ListDirection.java new file mode 100644 index 0000000000..ebbf5961e4 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ListDirection.java @@ -0,0 +1,19 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.ListBaseCommands; + +/** + * Enumeration representing element popping or adding direction for the {@link ListBaseCommands} + * commands. + */ +public enum ListDirection { + /** + * Represents the option that elements should be popped from or added to the left side of a list. + */ + LEFT, + /** + * Represents the option that elements should be popped from or added to the right side of a list. + */ + RIGHT +} diff --git a/java/client/src/main/java/glide/api/models/commands/RangeOptions.java b/java/client/src/main/java/glide/api/models/commands/RangeOptions.java index 179145eaaf..513c5be317 100644 --- a/java/client/src/main/java/glide/api/models/commands/RangeOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/RangeOptions.java @@ -1,10 +1,13 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_VALKEY_API; +import static glide.api.models.GlideString.gs; import static glide.utils.ArrayTransformUtils.concatenateArrays; import glide.api.commands.SortedSetBaseCommands; +import glide.api.models.GlideString; +import glide.utils.ArgsBuilder; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -16,13 +19,13 @@ * SortedSetBaseCommands#zrange}, {@link SortedSetBaseCommands#zrangestore}, {@link * SortedSetBaseCommands#zrangeWithScores}, and {@link SortedSetBaseCommands#zlexcount} * - * @see redis.io - * @see redis.io - * @see redis.io - * @see redis.io - * @see redis.io - * @see redis.io - * @see redis.io + * @see valkey.io + * @see valkey.io + * @see valkey.io + * @see valkey.io + * @see valkey.io + * @see valkey.io + * @see valkey.io */ public class RangeOptions { @@ -44,10 +47,10 @@ public enum InfScoreBound implements ScoreRange { POSITIVE_INFINITY("+inf"), NEGATIVE_INFINITY("-inf"); - private final String redisApi; + private final String valkeyApi; public String toArgs() { - return redisApi; + return valkeyApi; } } @@ -76,7 +79,7 @@ public ScoreBoundary(double bound) { this(bound, true); } - /** Convert the score boundary to the Redis protocol format. */ + /** Convert the score boundary to the Valkey protocol format. */ public String toArgs() { return (isInclusive ? "" : "(") + bound; } @@ -102,11 +105,11 @@ public enum InfLexBound implements LexRange { POSITIVE_INFINITY("+"), NEGATIVE_INFINITY("-"); - private final String redisApi; + private final String valkeyApi; @Override public String toArgs() { - return redisApi; + return valkeyApi; } } @@ -135,7 +138,7 @@ public LexBoundary(@NonNull String value) { this(value, true); } - /** Convert the lex boundary to the Redis protocol format. */ + /** Convert the lex boundary to the Valkey protocol format. */ @Override public String toArgs() { return (isInclusive ? "[" : "(") + value; @@ -302,9 +305,17 @@ public RangeByScore( public static String[] createZRangeArgs( String key, RangeQuery rangeQuery, boolean reverse, boolean withScores) { String[] arguments = - concatenateArrays(new String[] {key}, createZRangeBaseArgs(rangeQuery, reverse)); + concatenateArrays( + new String[] {key}, createZRangeBaseArgs(rangeQuery, reverse, withScores)); + return arguments; + } + + public static GlideString[] createZRangeArgsBinary( + GlideString key, RangeQuery rangeQuery, boolean reverse, boolean withScores) { + GlideString[] arguments = + concatenateArrays(new GlideString[] {key}, createZRangeBaseArgsBinary(rangeQuery, reverse)); if (withScores) { - arguments = concatenateArrays(arguments, new String[] {WITH_SCORES_REDIS_API}); + arguments = concatenateArrays(arguments, new GlideString[] {gs(WITH_SCORES_VALKEY_API)}); } return arguments; @@ -313,10 +324,17 @@ public static String[] createZRangeArgs( public static String[] createZRangeStoreArgs( String destination, String source, RangeQuery rangeQuery, boolean reverse) { return concatenateArrays( - new String[] {destination, source}, createZRangeBaseArgs(rangeQuery, reverse)); + new String[] {destination, source}, createZRangeBaseArgs(rangeQuery, reverse, false)); + } + + public static GlideString[] createZRangeStoreArgsBinary( + GlideString destination, GlideString source, RangeQuery rangeQuery, boolean reverse) { + return concatenateArrays( + new GlideString[] {destination, source}, createZRangeBaseArgsBinary(rangeQuery, reverse)); } - private static String[] createZRangeBaseArgs(RangeQuery rangeQuery, boolean reverse) { + public static String[] createZRangeBaseArgs( + RangeQuery rangeQuery, boolean reverse, boolean withScores) { String[] arguments = new String[] {rangeQuery.getStart(), rangeQuery.getEnd()}; if (rangeQuery instanceof RangeByScore) { @@ -340,6 +358,28 @@ private static String[] createZRangeBaseArgs(RangeQuery rangeQuery, boolean reve }); } + if (withScores) { + arguments = concatenateArrays(arguments, new String[] {WITH_SCORES_VALKEY_API}); + } + return arguments; } + + public static GlideString[] createZRangeBaseArgsBinary(RangeQuery rangeQuery, boolean reverse) { + ArgsBuilder builder = new ArgsBuilder().add(rangeQuery.getStart()).add(rangeQuery.getEnd()); + + builder + .addIf("BYSCORE", rangeQuery instanceof RangeByScore) + .addIf("BYLEX", rangeQuery instanceof RangeByLex) + .addIf("REV", reverse); + + if (rangeQuery.getLimit() != null) { + builder + .add("LIMIT") + .add(rangeQuery.getLimit().getOffset()) + .add(rangeQuery.getLimit().getCount()); + } + + return builder.toArray(); + } } diff --git a/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java b/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java new file mode 100644 index 0000000000..4a069e5a92 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/RestoreOptions.java @@ -0,0 +1,91 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import static glide.api.models.GlideString.gs; + +import glide.api.commands.GenericBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import lombok.*; + +/** + * Optional arguments to {@link GenericBaseCommands#restore(GlideString, long, byte[], + * RestoreOptions)} + * + * @see valkey.io + */ +@Getter +@Builder +public final class RestoreOptions { + /** REPLACE subcommand string to replace existing key */ + public static final String REPLACE_VALKEY_API = "REPLACE"; + + /** + * ABSTTL subcommand string to represent absolute timestamp (in milliseconds) for TTL + */ + public static final String ABSTTL_VALKEY_API = "ABSTTL"; + + /** IDELTIME subcommand string to set Object Idletime */ + public static final String IDLETIME_VALKEY_API = "IDLETIME"; + + /** FREQ subcommand string to set Object Frequency */ + public static final String FREQ_VALKEY_API = "FREQ"; + + /** When `true`, it represents REPLACE keyword has been used */ + @Builder.Default private boolean hasReplace = false; + + /** When `true`, it represents ABSTTL keyword has been used */ + @Builder.Default private boolean hasAbsttl = false; + + /** It represents the idletime of object */ + @Builder.Default private Long idletime = null; + + /** It represents the frequency of object */ + @Builder.Default private Long frequency = null; + + /** + * Creates the argument to be used in {@link GenericBaseCommands#restore(GlideString, long, + * byte[], RestoreOptions)} + * + * @return a GlideString array that holds the subcommands and their arguments. + */ + public GlideString[] toArgs(GlideString key, long ttl, byte[] value) { + List resultList = new ArrayList<>(); + + resultList.add(key); + resultList.add(gs(Long.toString(ttl))); + resultList.add(gs(value)); + + if (hasReplace) { + resultList.add(gs(REPLACE_VALKEY_API)); + } + + if (hasAbsttl) { + resultList.add(gs(ABSTTL_VALKEY_API)); + } + + if (idletime != null) { + resultList.add(gs(IDLETIME_VALKEY_API)); + resultList.add(gs(Long.toString(idletime))); + } + + if (frequency != null) { + resultList.add(gs(FREQ_VALKEY_API)); + resultList.add(gs(Long.toString(frequency))); + } + + return resultList.toArray(new GlideString[0]); + } + + /** Custom setter methods for replace and absttl */ + public static class RestoreOptionsBuilder { + public RestoreOptionsBuilder replace() { + return hasReplace(true); + } + + public RestoreOptionsBuilder absttl() { + return hasAbsttl(true); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/ScoreFilter.java b/java/client/src/main/java/glide/api/models/commands/ScoreFilter.java new file mode 100644 index 0000000000..4fc0c92e58 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ScoreFilter.java @@ -0,0 +1,16 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.SortedSetBaseCommands; + +/** + * Mandatory option for {@link SortedSetBaseCommands#bzmpop} and for {@link + * SortedSetBaseCommands#zmpop}.
+ * Defines which elements to pop from the sorted set. + */ +public enum ScoreFilter { + /** Pop elements with the lowest scores. */ + MIN, + /** Pop elements with the highest scores. */ + MAX +} diff --git a/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java b/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java index 6aef640569..dbdba5a461 100644 --- a/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/ScriptOptions.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; import glide.api.commands.GenericBaseCommands; @@ -11,7 +11,7 @@ /** * Optional arguments for {@link GenericBaseCommands#invokeScript(Script, ScriptOptions)} command. * - * @see redis.io + * @see valkey.io */ @Builder public final class ScriptOptions { diff --git a/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java b/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java new file mode 100644 index 0000000000..50535904f4 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ScriptOptionsGlideString.java @@ -0,0 +1,26 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import glide.api.models.GlideString; +import glide.api.models.Script; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +/** + * Optional arguments for {@link GenericBaseCommands#invokeScript(Script, ScriptOptionsGlideString)} + * command. + * + * @see valkey.io + */ +@Builder +public class ScriptOptionsGlideString { + + /** The keys that are used in the script. */ + @Singular @Getter private final List keys; + + /** The arguments for the script. */ + @Singular @Getter private final List args; +} diff --git a/java/client/src/main/java/glide/api/models/commands/SetOptions.java b/java/client/src/main/java/glide/api/models/commands/SetOptions.java index 831e29c1b1..ccee370ff2 100644 --- a/java/client/src/main/java/glide/api/models/commands/SetOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/SetOptions.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; import static glide.api.models.commands.SetOptions.ExpiryType.KEEP_EXISTING; @@ -7,18 +7,18 @@ import static glide.api.models.commands.SetOptions.ExpiryType.UNIX_MILLISECONDS; import static glide.api.models.commands.SetOptions.ExpiryType.UNIX_SECONDS; +import command_request.CommandRequestOuterClass.Command; import glide.api.commands.StringBaseCommands; import java.util.ArrayList; import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; -import redis_request.RedisRequestOuterClass.Command; /** * Optional arguments for {@link StringBaseCommands#set(String, String, SetOptions)} command. * - * @see redis.io + * @see valkey.io */ @Builder public final class SetOptions { @@ -32,7 +32,7 @@ public final class SetOptions { /** * Set command to return the old string stored at key, or null if * key did not exist. An error is returned and SET aborted if the value stored - * at key is not a string. Equivalent to GET in the Redis API. + * at key is not a string. Equivalent to GET in the Valkey API. */ private final boolean returnOldValue; @@ -43,15 +43,15 @@ public final class SetOptions { @RequiredArgsConstructor @Getter public enum ConditionalSet { - /** Only set the key if it already exists. Equivalent to XX in the Redis API. */ + /** Only set the key if it already exists. Equivalent to XX in the Valkey API. */ ONLY_IF_EXISTS("XX"), /** - * Only set the key if it does not already exist. Equivalent to NX in the Redis + * Only set the key if it does not already exist. Equivalent to NX in the Valkey * API. */ ONLY_IF_DOES_NOT_EXIST("NX"); - private final String redisApi; + private final String valkeyApi; } /** Configuration of value lifetime. */ @@ -77,14 +77,14 @@ private Expiry(ExpiryType type, Long count) { /** * Retain the time to live associated with the key. Equivalent to KEEPTTL in the - * Redis API. + * Valkey API. */ public static Expiry KeepExisting() { return new Expiry(KEEP_EXISTING); } /** - * Set the specified expire time, in seconds. Equivalent to EX in the Redis API. + * Set the specified expire time, in seconds. Equivalent to EX in the Valkey API. * * @param seconds time to expire, in seconds * @return Expiry @@ -94,7 +94,7 @@ public static Expiry Seconds(Long seconds) { } /** - * Set the specified expire time, in milliseconds. Equivalent to PX in the Redis + * Set the specified expire time, in milliseconds. Equivalent to PX in the Valkey * API. * * @param milliseconds time to expire, in milliseconds @@ -106,7 +106,7 @@ public static Expiry Milliseconds(Long milliseconds) { /** * Set the specified Unix time at which the key will expire, in seconds. Equivalent to - * EXAT in the Redis API. + * EXAT in the Valkey API. * * @param unixSeconds UNIX TIME to expire, in seconds. * @return Expiry @@ -117,7 +117,7 @@ public static Expiry UnixSeconds(Long unixSeconds) { /** * Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to - * PXAT in the Redis API. + * PXAT in the Valkey API. * * @param unixMilliseconds UNIX TIME to expire, in milliseconds. * @return Expiry @@ -136,7 +136,7 @@ protected enum ExpiryType { UNIX_SECONDS("EXAT"), UNIX_MILLISECONDS("PXAT"); - private final String redisApi; + private final String valkeyApi; } /** String representation of {@link #returnOldValue} when set. */ @@ -150,7 +150,7 @@ protected enum ExpiryType { public String[] toArgs() { List optionArgs = new ArrayList<>(); if (conditionalSet != null) { - optionArgs.add(conditionalSet.redisApi); + optionArgs.add(conditionalSet.valkeyApi); } if (returnOldValue) { @@ -158,7 +158,7 @@ public String[] toArgs() { } if (expiry != null) { - optionArgs.add(expiry.type.redisApi); + optionArgs.add(expiry.type.valkeyApi); if (expiry.type != KEEP_EXISTING) { assert expiry.count != null : "Set command received expiry type " + expiry.type + ", but count was not set."; diff --git a/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java new file mode 100644 index 0000000000..8140febe5f --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java @@ -0,0 +1,109 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to sort, sortReadOnly, and sortStore commands + * + * @see valkey.io and valkey.io + */ +@SuperBuilder +public abstract class SortBaseOptions { + /** + * LIMIT subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String LIMIT_COMMAND_STRING = "LIMIT"; + + /** + * ALPHA subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String ALPHA_COMMAND_STRING = "ALPHA"; + + /** STORE subcommand string to include in the SORT command. */ + public static final String STORE_COMMAND_STRING = "STORE"; + + /** + * Limiting the range of the query by setting offset and result count. See {@link Limit} class for + * more information. + */ + private final Limit limit; + + /** Options for sorting order of elements. */ + private final OrderBy orderBy; + + /** + * When true, sorts elements lexicographically. When false (default), + * sorts elements numerically. Use this when the list, set, or sorted set contains string values + * that cannot be converted into double precision floating point numbers. + */ + private final boolean isAlpha; + + public abstract static class SortBaseOptionsBuilder< + C extends SortBaseOptions, B extends SortBaseOptionsBuilder> { + public B alpha() { + this.isAlpha = true; + return self(); + } + } + + /** + * The LIMIT argument is commonly used to specify a subset of results from the + * matching elements, similar to the LIMIT clause in SQL (e.g., `SELECT LIMIT offset, + * count`). + */ + @RequiredArgsConstructor + public static final class Limit { + /** The starting position of the range, zero based. */ + private final long offset; + + /** + * The maximum number of elements to include in the range. A negative count returns all elements + * from the offset. + */ + private final long count; + } + + /** + * Specifies the order to sort the elements. Can be ASC (ascending) or DESC + * (descending). + */ + @RequiredArgsConstructor + public enum OrderBy { + ASC, + DESC + } + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (limit != null) { + optionArgs.addAll( + List.of( + LIMIT_COMMAND_STRING, + Long.toString(this.limit.offset), + Long.toString(this.limit.count))); + } + + if (orderBy != null) { + optionArgs.add(this.orderBy.toString()); + } + + if (isAlpha) { + optionArgs.add(ALPHA_COMMAND_STRING); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java new file mode 100644 index 0000000000..456f940461 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java @@ -0,0 +1,15 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericClusterCommands#sort(String, SortClusterOptions)}, {@link + * GenericClusterCommands#sort(GlideString, SortClusterOptions)}, {@link + * GenericClusterCommands#sortReadOnly(String, SortClusterOptions)}, {@link + * GenericClusterCommands#sortReadOnly(GlideString, SortClusterOptions)}, {@link + * GenericClusterCommands#sortStore(String, String, SortClusterOptions)} and {@link + * GenericClusterCommands#sortStore(GlideString, GlideString, SortClusterOptions)}, + */ +@SuperBuilder +public class SortClusterOptions extends SortBaseOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/SortOptions.java b/java/client/src/main/java/glide/api/models/commands/SortOptions.java new file mode 100644 index 0000000000..58c6bb07fb --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOptions.java @@ -0,0 +1,77 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Singular; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericCommands#sort(String, SortOptions)}, {@link + * GenericCommands#sortReadOnly(String, SortOptions)}, and {@link GenericCommands#sortStore(String, + * String, SortOptions)} + * + * @see valkey.io and valkey.io + */ +@SuperBuilder +public class SortOptions extends SortBaseOptions { + /** + * BY subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String BY_COMMAND_STRING = "BY"; + + /** + * GET subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String GET_COMMAND_STRING = "GET"; + + /** + * A pattern to sort by external keys instead of by the elements stored at the key themselves. The + * pattern should contain an asterisk (*) as a placeholder for the element values, where the value + * from the key replaces the asterisk to create the key name. For example, if key + * contains IDs of objects, byPattern can be used to sort these IDs based on an + * attribute of the objects, like their weights or timestamps. + */ + private final String byPattern; + + /** + * A pattern used to retrieve external keys' values, instead of the elements at key. + * The pattern should contain an asterisk (*) as a placeholder for the element values, where the + * value from key replaces the asterisk to create the key name. This + * allows the sorted elements to be transformed based on the related keys values. For example, if + * key contains IDs of users, getPatterns can be used to retrieve + * specific attributes of these users, such as their names or email addresses. E.g., if + * getPatterns is name_*, the command will return the values of the keys + * name_<element> for each sorted element. Multiple getPatterns + * arguments can be provided to retrieve multiple attributes. The special value # can + * be used to include the actual element from key being sorted. If not provided, only + * the sorted elements themselves are returned.
+ * + * @see valkey.io for more information. + */ + @Singular private final List getPatterns; + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(List.of(super.toArgs())); + + if (byPattern != null) { + optionArgs.addAll(List.of(BY_COMMAND_STRING, byPattern)); + } + + if (getPatterns != null) { + getPatterns.stream() + .forEach(getPattern -> optionArgs.addAll(List.of(GET_COMMAND_STRING, getPattern))); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/SortOptionsBinary.java new file mode 100644 index 0000000000..0741f36c0f --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOptionsBinary.java @@ -0,0 +1,83 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import static glide.api.models.GlideString.gs; + +import glide.api.commands.GenericCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import lombok.Singular; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericCommands#sort(GlideString, SortOptionsBinary)}, {@link + * GenericCommands#sortReadOnly(GlideString, SortOptionsBinary)}, and {@link + * GenericCommands#sortStore(GlideString, String, SortOptionsBinary)} + * + * @see valkey.io and valkey.io + */ +@SuperBuilder +public class SortOptionsBinary extends SortBaseOptions { + /** + * BY subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final GlideString BY_COMMAND_GLIDE_STRING = gs("BY"); + + /** + * GET subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final GlideString GET_COMMAND_GLIDE_STRING = gs("GET"); + + /** + * A pattern to sort by external keys instead of by the elements stored at the key themselves. The + * pattern should contain an asterisk (*) as a placeholder for the element values, where the value + * from the key replaces the asterisk to create the key name. For example, if key + * contains IDs of objects, byPattern can be used to sort these IDs based on an + * attribute of the objects, like their weights or timestamps. + */ + private final GlideString byPattern; + + /** + * A pattern used to retrieve external keys' values, instead of the elements at key. + * The pattern should contain an asterisk (*) as a placeholder for the element values, where the + * value from key replaces the asterisk to create the key name. This + * allows the sorted elements to be transformed based on the related keys values. For example, if + * key contains IDs of users, getPatterns can be used to retrieve + * specific attributes of these users, such as their names or email addresses. E.g., if + * getPatterns is name_*, the command will return the values of the keys + * name_<element> for each sorted element. Multiple getPatterns + * arguments can be provided to retrieve multiple attributes. The special value # can + * be used to include the actual element from key being sorted. If not provided, only + * the sorted elements themselves are returned.
+ * + * @see valkey.io for more information. + */ + @Singular private final List getPatterns; + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(List.of(super.toArgs())); + + if (byPattern != null) { + optionArgs.addAll(List.of(BY_COMMAND_GLIDE_STRING.toString(), byPattern.toString())); + } + + if (getPatterns != null) { + getPatterns.stream() + .forEach( + getPattern -> + optionArgs.addAll( + List.of(GET_COMMAND_GLIDE_STRING.toString(), getPattern.toString()))); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortOrder.java b/java/client/src/main/java/glide/api/models/commands/SortOrder.java new file mode 100644 index 0000000000..09bdbb8b83 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOrder.java @@ -0,0 +1,10 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +/** Defines the sort order for nested results. */ +public enum SortOrder { + /** Sort by ascending order. */ + ASC, + /** Sort by descending order. */ + DESC +} diff --git a/java/client/src/main/java/glide/api/models/commands/StreamAddOptions.java b/java/client/src/main/java/glide/api/models/commands/StreamAddOptions.java deleted file mode 100644 index 34509523ed..0000000000 --- a/java/client/src/main/java/glide/api/models/commands/StreamAddOptions.java +++ /dev/null @@ -1,178 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api.models.commands; - -import glide.api.commands.StreamBaseCommands; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import lombok.Builder; -import lombok.NonNull; - -/** - * Optional arguments to {@link StreamBaseCommands#xadd(String, Map, StreamAddOptions)} - * - * @see redis.io - */ -@Builder -public final class StreamAddOptions { - - public static final String NO_MAKE_STREAM_REDIS_API = "NOMKSTREAM"; - public static final String ID_WILDCARD_REDIS_API = "*"; - public static final String TRIM_MAXLEN_REDIS_API = "MAXLEN"; - public static final String TRIM_MINID_REDIS_API = "MINID"; - public static final String TRIM_EXACT_REDIS_API = "="; - public static final String TRIM_NOT_EXACT_REDIS_API = "~"; - public static final String TRIM_LIMIT_REDIS_API = "LIMIT"; - - /** If set, the new entry will be added with this id. */ - private final String id; - - /** - * If set to false, a new stream won't be created if no stream matches the given key. - *
- * Equivalent to NOMKSTREAM in the Redis API. - */ - private final Boolean makeStream; - - /** If set, the add operation will also trim the older entries in the stream. */ - private final StreamTrimOptions trim; - - public abstract static class StreamTrimOptions { - /** - * If true, the stream will be trimmed exactly. Equivalent to = in the - * Redis API. Otherwise, the stream will be trimmed in a near-exact manner, which is more - * efficient, equivalent to ~ in the Redis API. - */ - protected boolean exact; - - /** If set, sets the maximal amount of entries that will be deleted. */ - protected Long limit; - - protected abstract String getMethod(); - - protected abstract String getThreshold(); - - protected List getRedisApi() { - List optionArgs = new ArrayList<>(); - - optionArgs.add(this.getMethod()); - optionArgs.add(this.exact ? TRIM_EXACT_REDIS_API : TRIM_NOT_EXACT_REDIS_API); - optionArgs.add(this.getThreshold()); - - if (this.limit != null) { - optionArgs.add(TRIM_LIMIT_REDIS_API); - optionArgs.add(this.limit.toString()); - } - - return optionArgs; - } - } - - /** Option to trim the stream according to minimum ID. */ - public static class MinId extends StreamTrimOptions { - /** Trim the stream according to entry ID. Equivalent to MINID in the Redis API. */ - private final String threshold; - - /** - * Create a trim option to trim stream based on stream ID. - * - * @param exact Whether to match exactly on the threshold. - * @param threshold Comparison id. - */ - public MinId(boolean exact, @NonNull String threshold) { - this.threshold = threshold; - this.exact = exact; - } - - /** - * Create a trim option to trim stream based on stream ID. - * - * @param exact Whether to match exactly on the threshold. - * @param threshold Comparison id. - * @param limit Max number of stream entries to be trimmed. - */ - public MinId(boolean exact, @NonNull String threshold, long limit) { - this.threshold = threshold; - this.exact = exact; - this.limit = limit; - } - - @Override - protected String getMethod() { - return TRIM_MINID_REDIS_API; - } - - @Override - protected String getThreshold() { - return threshold; - } - } - - /** Option to trim the stream according to maximum stream length. */ - public static class MaxLen extends StreamTrimOptions { - /** - * Trim the stream according to length.
- * Equivalent to MAXLEN in the Redis API. - */ - private final Long threshold; - - /** - * Create a Max Length trim option to trim stream based on length. - * - * @param exact Whether to match exactly on the threshold. - * @param threshold Comparison count. - */ - public MaxLen(boolean exact, long threshold) { - this.threshold = threshold; - this.exact = exact; - } - - /** - * Create a Max Length trim option to trim stream entries exceeds the threshold. - * - * @param exact Whether to match exactly on the threshold. - * @param threshold Comparison count. - * @param limit Max number of stream entries to be trimmed. - */ - public MaxLen(boolean exact, long threshold, long limit) { - this.threshold = threshold; - this.exact = exact; - this.limit = limit; - } - - @Override - protected String getMethod() { - return TRIM_MAXLEN_REDIS_API; - } - - @Override - protected String getThreshold() { - return threshold.toString(); - } - } - - /** - * Converts options for Xadd into a String[]. - * - * @return String[] - */ - public String[] toArgs() { - List optionArgs = new ArrayList<>(); - - if (makeStream != null && !makeStream) { - optionArgs.add(NO_MAKE_STREAM_REDIS_API); - } - - if (trim != null) { - optionArgs.addAll(trim.getRedisApi()); - } - - if (id != null) { - optionArgs.add(id); - } else { - optionArgs.add(ID_WILDCARD_REDIS_API); - } - - return optionArgs.toArray(new String[0]); - } -} diff --git a/java/client/src/main/java/glide/api/models/commands/WeightAggregateOptions.java b/java/client/src/main/java/glide/api/models/commands/WeightAggregateOptions.java new file mode 100644 index 0000000000..637731719a --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/WeightAggregateOptions.java @@ -0,0 +1,152 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import static glide.utils.ArrayTransformUtils.concatenateArrays; + +import glide.api.commands.SortedSetBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; + +/** + * Arguments for {@link SortedSetBaseCommands#zunion}, {@link + * SortedSetBaseCommands#zunionWithScores}, {@link SortedSetBaseCommands#zinter}, {@link + * SortedSetBaseCommands#zinterWithScores}, {@link SortedSetBaseCommands#zunionstore}, and {@link + * SortedSetBaseCommands#zinterstore}. + * + * @see valkey.io for more details. + * @see valkey.io for more details. + * @see valkey.io for more details. + * @see valkey.io for more details. + */ +public abstract class WeightAggregateOptions { + public static final String WEIGHTS_VALKEY_API = "WEIGHTS"; + public static final String AGGREGATE_VALKEY_API = "AGGREGATE"; + + /** + * Option for the method of aggregating scores from multiple sets. This option defaults to SUM if + * not specified. + */ + public enum Aggregate { + /** Aggregates by summing the scores of each element across sets. */ + SUM, + /** Aggregates by selecting the minimum score for each element across sets. */ + MIN, + /** Aggregates by selecting the maximum score for each element across sets. */ + MAX; + + public String[] toArgs() { + return new String[] {AGGREGATE_VALKEY_API, toString()}; + } + } + + /** + * Basic interface. Please use one of the following implementations: + * + *
    + *
  • {@link KeyArray} + *
  • {@link WeightedKeys} + *
+ */ + public interface KeysOrWeightedKeys { + /** Convert to command arguments according to the Valkey API. */ + String[] toArgs(); + } + + /** Represents the keys of the sorted sets involved in the aggregation operation. */ + @RequiredArgsConstructor + public static class KeyArray implements KeysOrWeightedKeys { + private final String[] keys; + + @Override + public String[] toArgs() { + return concatenateArrays(new String[] {Integer.toString(keys.length)}, keys); + } + } + + /** + * Represents the mapping of sorted set keys to their score weights. Each weight is used to boost + * the scores of elements in the corresponding sorted set by multiplying them before their scores + * are aggregated. + */ + @RequiredArgsConstructor + public static class WeightedKeys implements KeysOrWeightedKeys { + private final List> keysWeights; + + @Override + public String[] toArgs() { + List keys = new ArrayList<>(); + List weights = new ArrayList<>(); + List argumentsList = new ArrayList<>(); + + for (Pair entry : keysWeights) { + keys.add(entry.getLeft()); + weights.add(entry.getRight()); + } + argumentsList.add(Integer.toString(keys.size())); + argumentsList.addAll(keys); + argumentsList.add(WEIGHTS_VALKEY_API); + for (Double weight : weights) { + argumentsList.add(weight.toString()); + } + + return argumentsList.toArray(new String[0]); + } + } + + /** + * Basic interface. Please use one of the following implementations: + * + *
    + *
  • {@link KeyArray} + *
  • {@link WeightedKeys} + *
+ */ + public interface KeysOrWeightedKeysBinary { + /** Convert to command arguments according to the Valkey API. */ + GlideString[] toArgs(); + } + + /** Represents the keys of the sorted sets involved in the aggregation operation. */ + @RequiredArgsConstructor + public static class KeyArrayBinary implements KeysOrWeightedKeysBinary { + private final GlideString[] keys; + + @Override + public GlideString[] toArgs() { + return concatenateArrays(new GlideString[] {GlideString.of(keys.length)}, keys); + } + } + + /** + * Represents the mapping of sorted set keys to their score weights. Each weight is used to boost + * the scores of elements in the corresponding sorted set by multiplying them before their scores + * are aggregated. + */ + @RequiredArgsConstructor + public static class WeightedKeysBinary implements KeysOrWeightedKeysBinary { + private final List> keysWeights; + + @Override + public GlideString[] toArgs() { + List keys = new ArrayList<>(); + List weights = new ArrayList<>(); + List argumentsList = new ArrayList<>(); + + for (Pair entry : keysWeights) { + keys.add(entry.getLeft()); + weights.add(entry.getRight()); + } + argumentsList.add(GlideString.of(keys.size())); + argumentsList.addAll(keys); + argumentsList.add(GlideString.of(WEIGHTS_VALKEY_API)); + for (Double weight : weights) { + argumentsList.add(GlideString.of(weight)); + } + + return argumentsList.toArray(new GlideString[0]); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/ZaddOptions.java b/java/client/src/main/java/glide/api/models/commands/ZAddOptions.java similarity index 67% rename from java/client/src/main/java/glide/api/models/commands/ZaddOptions.java rename to java/client/src/main/java/glide/api/models/commands/ZAddOptions.java index 625e3b6f7b..a074a9dd6f 100644 --- a/java/client/src/main/java/glide/api/models/commands/ZaddOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/ZAddOptions.java @@ -1,7 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.commands; import glide.api.commands.SortedSetBaseCommands; +import glide.api.models.GlideString; +import glide.utils.ArgsBuilder; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -9,14 +11,14 @@ import lombok.RequiredArgsConstructor; /** - * Optional arguments to {@link SortedSetBaseCommands#zadd(String, Map, ZaddOptions, boolean)}, - * {@link SortedSetBaseCommands#zadd(String, Map, ZaddOptions)} and {@link - * SortedSetBaseCommands#zaddIncr(String, String, double, ZaddOptions)} + * Optional arguments to {@link SortedSetBaseCommands#zadd(String, Map, ZAddOptions, boolean)}, + * {@link SortedSetBaseCommands#zadd(String, Map, ZAddOptions)} and {@link + * SortedSetBaseCommands#zaddIncr(String, String, double, ZAddOptions)} * - * @see redis.io + * @see valkey.io */ @Builder -public final class ZaddOptions { +public final class ZAddOptions { /** * Defines conditions for updating or adding elements with {@link SortedSetBaseCommands#zadd} * command. @@ -32,32 +34,32 @@ public final class ZaddOptions { public enum ConditionalChange { /** * Only update elements that already exist. Don't add new elements. Equivalent to XX - * in the Redis API. + * in the Valkey API. */ ONLY_IF_EXISTS("XX"), /** * Only add new elements. Don't update already existing elements. Equivalent to NX - * in the Redis API. + * in the Valkey API. */ ONLY_IF_DOES_NOT_EXIST("NX"); - private final String redisApi; + private final String valkeyApi; } @RequiredArgsConstructor public enum UpdateOptions { /** * Only update existing elements if the new score is less than the current score. Equivalent to - * LT in the Redis API. + * LT in the Valkey API. */ SCORE_LESS_THAN_CURRENT("LT"), /** * Only update existing elements if the new score is greater than the current score. Equivalent - * to GT in the Redis API. + * to GT in the Valkey API. */ SCORE_GREATER_THAN_CURRENT("GT"); - private final String redisApi; + private final String valkeyApi; } /** @@ -69,20 +71,29 @@ public String[] toArgs() { if (conditionalChange == ConditionalChange.ONLY_IF_DOES_NOT_EXIST && updateOptions != null) { throw new IllegalArgumentException( "The GT, LT, and NX options are mutually exclusive. Cannot choose both " - + updateOptions.redisApi + + updateOptions.valkeyApi + " and NX."); } List optionArgs = new ArrayList<>(); if (conditionalChange != null) { - optionArgs.add(conditionalChange.redisApi); + optionArgs.add(conditionalChange.valkeyApi); } if (updateOptions != null) { - optionArgs.add(updateOptions.redisApi); + optionArgs.add(updateOptions.valkeyApi); } return optionArgs.toArray(new String[0]); } + + /** + * Converts ZaddOptions into a GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toArgsBinary() { + return new ArgsBuilder().add(toArgs()).toArray(); + } } diff --git a/java/client/src/main/java/glide/api/models/commands/bitmap/BitFieldOptions.java b/java/client/src/main/java/glide/api/models/commands/bitmap/BitFieldOptions.java new file mode 100644 index 0000000000..1a465150c3 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/bitmap/BitFieldOptions.java @@ -0,0 +1,251 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.bitmap; + +import static glide.utils.ArrayTransformUtils.concatenateArrays; + +import glide.api.commands.BitmapBaseCommands; +import glide.api.models.GlideString; +import java.util.Arrays; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Subcommand arguments for {@link BitmapBaseCommands#bitfield(String, BitFieldSubCommands[])} and + * {@link BitmapBaseCommands#bitfieldReadOnly(String, BitFieldReadOnlySubCommands[])}. Specifies + * subcommands, bit-encoding type, and offset type. + * + * @see valkey.io and valkey.io + */ +public class BitFieldOptions { + /** GET subcommand string to include in the BITFIELD command. */ + public static final String GET_COMMAND_STRING = "GET"; + + /** SET subcommand string to include in the BITFIELD command. */ + public static final String SET_COMMAND_STRING = "SET"; + + /** INCRBY subcommand string to include in the BITFIELD command. */ + public static final String INCRBY_COMMAND_STRING = "INCRBY"; + + /** OVERFLOW subcommand string to include in the BITFIELD command. */ + public static final String OVERFLOW_COMMAND_STRING = "OVERFLOW"; + + /** Prefix specifying that the encoding is unsigned. */ + public static final String UNSIGNED_ENCODING_PREFIX = "u"; + + /** Prefix specifying that the encoding is signed. */ + public static final String SIGNED_ENCODING_PREFIX = "i"; + + /** Prefix specifying that the offset uses an encoding multiplier. */ + public static final String OFFSET_MULTIPLIER_PREFIX = "#"; + + /** Subcommands for bitfield and bitfield_ro. */ + public interface BitFieldSubCommands { + /** + * Creates the subcommand arguments. + * + * @return a String array with subcommands and their arguments. + */ + String[] toArgs(); + } + + /** Subcommands for bitfieldReadOnly. */ + public interface BitFieldReadOnlySubCommands extends BitFieldSubCommands {} + + /** + * GET subcommand for getting the value in the binary representation of the string + * stored in key based on {@link BitEncoding} and {@link Offset}. + */ + @RequiredArgsConstructor + public static final class BitFieldGet implements BitFieldReadOnlySubCommands { + /** Specifies if the bit encoding is signed or unsigned. */ + private final BitEncoding encoding; + + /** Specifies if the offset uses an encoding multiplier. */ + private final BitOffset offset; + + public String[] toArgs() { + return new String[] {GET_COMMAND_STRING, encoding.getEncoding(), offset.getOffset()}; + } + } + + /** + * SET subcommand for setting the bits in the binary representation of the string + * stored in key based on {@link BitEncoding} and {@link Offset}. + */ + @RequiredArgsConstructor + public static final class BitFieldSet implements BitFieldSubCommands { + /** Specifies if the bit encoding is signed or unsigned. */ + private final BitEncoding encoding; + + /** Specifies if the offset uses an encoding multiplier. */ + private final BitOffset offset; + + /** Value to set the bits in the binary value to. */ + private final long value; + + public String[] toArgs() { + return new String[] { + SET_COMMAND_STRING, encoding.getEncoding(), offset.getOffset(), Long.toString(value) + }; + } + } + + /** + * INCRBY subcommand for increasing or decreasing the bits in the binary + * representation of the string stored in key based on {@link BitEncoding} and {@link + * Offset}. + */ + @RequiredArgsConstructor + public static final class BitFieldIncrby implements BitFieldSubCommands { + /** Specifies if the bit encoding is signed or unsigned. */ + private final BitEncoding encoding; + + /** Specifies if the offset uses an encoding multiplier. */ + private final BitOffset offset; + + /** Value to increment the bits in the binary value by. */ + private final long increment; + + public String[] toArgs() { + return new String[] { + INCRBY_COMMAND_STRING, encoding.getEncoding(), offset.getOffset(), Long.toString(increment) + }; + } + } + + /** + * OVERFLOW subcommand that determines the result of the SET or + * INCRBY commands when an under or overflow occurs. + */ + @RequiredArgsConstructor + public static final class BitFieldOverflow implements BitFieldSubCommands { + /** Overflow behaviour. */ + private final BitOverflowControl overflowControl; + + public String[] toArgs() { + return new String[] {OVERFLOW_COMMAND_STRING, overflowControl.toString()}; + } + + /** Supported bit overflow controls */ + public enum BitOverflowControl { + /** + * Performs modulo when overflow occurs with unsigned encoding. When overflows occur with + * signed encoding, the value restarts at the most negative value. When underflows occur with + * signed encoding, the value restarts at the most positive value. + */ + WRAP, + /** + * Underflows remain set to the minimum value and overflows remain set to the maximum value. + */ + SAT, + /** Returns null when overflows occur. */ + FAIL + } + } + + /** Specifies if the argument is a signed or unsigned encoding */ + private interface BitEncoding { + String getEncoding(); + } + + /** Specifies that the argument is a signed encoding. Must be less than 65 bits long. */ + public static final class SignedEncoding implements BitEncoding { + @Getter private final String encoding; + + /** + * Constructor that prepends the number with "i" to specify that it is in signed encoding. + * + * @param encodingLength bit size of encoding. + */ + public SignedEncoding(long encodingLength) { + encoding = SIGNED_ENCODING_PREFIX.concat(Long.toString(encodingLength)); + } + } + + /** Specifies that the argument is an unsigned encoding. Must be less than 64 bits long. */ + public static final class UnsignedEncoding implements BitEncoding { + @Getter private final String encoding; + + /** + * Constructor that prepends the number with "u" to specify that it is in unsigned encoding. + * + * @param encodingLength bit size of encoding. + */ + public UnsignedEncoding(long encodingLength) { + encoding = UNSIGNED_ENCODING_PREFIX.concat(Long.toString(encodingLength)); + } + } + + /** Offset in the array of bits. */ + private interface BitOffset { + String getOffset(); + } + + /** + * Offset in the array of bits. Must be greater than or equal to 0. If we have the binary 01101001 + * with offset of 1 for unsigned encoding of size 4, then the value is 13 from 0(1101)001. + */ + public static final class Offset implements BitOffset { + @Getter private final String offset; + + /** + * Constructor for Offset. + * + * @param offset element offset in the array of bits. + */ + public Offset(long offset) { + this.offset = Long.toString(offset); + } + } + + /** + * Offset in the array of bits multiplied by the encoding value. Must be greater than or equal to + * 0. If we have the binary 01101001 with offset multiplier of 1 for unsigned encoding of size 4, + * then the value is 9 from 0110(1001). + */ + public static final class OffsetMultiplier implements BitOffset { + @Getter private final String offset; + + /** + * Constructor for the offset multiplier. + * + * @param offset element offset multiplied by the encoding value in the array of bits. + */ + public OffsetMultiplier(long offset) { + this.offset = OFFSET_MULTIPLIER_PREFIX.concat(Long.toString(offset)); + } + } + + /** + * Creates the arguments to be used in {@link BitmapBaseCommands#bitfield(String, + * BitFieldSubCommands[])} and {@link BitmapBaseCommands#bitfieldReadOnly(String, + * BitFieldReadOnlySubCommands[])}. + * + * @param subCommands commands that holds arguments to be included in the argument String array. + * @return a String array that holds the sub commands and their arguments. + */ + public static String[] createBitFieldArgs(BitFieldSubCommands[] subCommands) { + String[] arguments = {}; + + for (int i = 0; i < subCommands.length; i++) { + arguments = concatenateArrays(arguments, subCommands[i].toArgs()); + } + + return arguments; + } + + /** + * Creates the arguments to be used in {@link BitmapBaseCommands#bitfield(GlideString, + * BitFieldSubCommands[])} and {@link BitmapBaseCommands#bitfieldReadOnly(GlideString, + * BitFieldReadOnlySubCommands[])}. + * + * @param subCommands commands that holds arguments to be included in the argument String array. + * @return a GlideString array that holds the sub commands and their arguments. + */ + public static GlideString[] createBitFieldGlideStringArgs(BitFieldSubCommands[] subCommands) { + return Arrays.stream(createBitFieldArgs(subCommands)) + .map(GlideString::gs) + .toArray(GlideString[]::new); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/bitmap/BitmapIndexType.java b/java/client/src/main/java/glide/api/models/commands/bitmap/BitmapIndexType.java new file mode 100644 index 0000000000..5ff1c5c330 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/bitmap/BitmapIndexType.java @@ -0,0 +1,17 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.bitmap; + +import glide.api.commands.BitmapBaseCommands; + +/** + * Optional arguments for {@link BitmapBaseCommands#bitcount(String, long, long, BitmapIndexType)}. + * Specifies if start and end arguments are BYTE indices or BIT indices + * + * @see valkey.io + */ +public enum BitmapIndexType { + /** Specifies a byte index * */ + BYTE, + /** Specifies a bit index */ + BIT +} diff --git a/java/client/src/main/java/glide/api/models/commands/bitmap/BitwiseOperation.java b/java/client/src/main/java/glide/api/models/commands/bitmap/BitwiseOperation.java new file mode 100644 index 0000000000..88ad339f14 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/bitmap/BitwiseOperation.java @@ -0,0 +1,17 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.bitmap; + +import glide.api.commands.BitmapBaseCommands; + +/** + * Defines bitwise operation for {@link BitmapBaseCommands#bitop(BitwiseOperation, String, + * String[])}. Specifies bitwise operation to perform between keys. + * + * @see valkey.io + */ +public enum BitwiseOperation { + AND, + OR, + XOR, + NOT +} diff --git a/java/client/src/main/java/glide/api/models/commands/function/FunctionListOptions.java b/java/client/src/main/java/glide/api/models/commands/function/FunctionListOptions.java new file mode 100644 index 0000000000..c129751422 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/function/FunctionListOptions.java @@ -0,0 +1,19 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.function; + +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.commands.ScriptingAndFunctionsCommands; + +/** + * Option for {@link ScriptingAndFunctionsCommands#functionList} and {@link + * ScriptingAndFunctionsClusterCommands#functionList} command. + * + * @see valkey.io + */ +public class FunctionListOptions { + /** Causes the server to include the libraries source implementation in the reply. */ + public static final String WITH_CODE_VALKEY_API = "WITHCODE"; + + /** Valkey API keyword followed by library name pattern. */ + public static final String LIBRARY_NAME_VALKEY_API = "LIBRARYNAME"; +} diff --git a/java/client/src/main/java/glide/api/models/commands/function/FunctionLoadOptions.java b/java/client/src/main/java/glide/api/models/commands/function/FunctionLoadOptions.java new file mode 100644 index 0000000000..c05482410f --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/function/FunctionLoadOptions.java @@ -0,0 +1,12 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.function; + +/** + * Option for FUNCTION LOAD command. + * + * @see valkey.io + */ +public enum FunctionLoadOptions { + /** Changes command behavior to overwrite the existing library with the new contents. */ + REPLACE +} diff --git a/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java b/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java new file mode 100644 index 0000000000..886c1ee1da --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java @@ -0,0 +1,30 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.function; + +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.commands.ScriptingAndFunctionsCommands; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; + +/** + * Option for FUNCTION RESTORE command: {@link + * ScriptingAndFunctionsCommands#functionRestore(byte[], FunctionRestorePolicy)}, {@link + * ScriptingAndFunctionsClusterCommands#functionRestore(byte[], FunctionRestorePolicy)}, and {@link + * ScriptingAndFunctionsClusterCommands#functionRestore(byte[], FunctionRestorePolicy, Route)}. + * + * @see valkey.io for details. + */ +public enum FunctionRestorePolicy { + /** + * Appends the restored libraries to the existing libraries and aborts on collision. This is the + * default policy. + */ + APPEND, + /** Deletes all existing libraries before restoring the payload. */ + FLUSH, + /** + * Appends the restored libraries to the existing libraries, replacing any existing ones in case + * of name collisions. Note that this policy doesn't prevent function name collisions, only + * libraries. + */ + REPLACE +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoAddOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoAddOptions.java new file mode 100644 index 0000000000..619c1794b7 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoAddOptions.java @@ -0,0 +1,81 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import glide.api.models.commands.ConditionalChange; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.Getter; + +/** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geoadd(String, Map, GeoAddOptions)} + * command. + * + * @see valkey.io + */ +@Getter +public final class GeoAddOptions { + public static final String CHANGED_VALKEY_API = "CH"; + + /** Options for handling existing members. See {@link ConditionalChange}. */ + private final ConditionalChange updateMode; + + /** If true, returns the count of changed elements instead of new elements added. */ + private final boolean changed; + + /** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geoadd(String, Map, GeoAddOptions)} + * command. + * + * @param updateMode Options for handling existing members. See {@link ConditionalChange} + * @param changed If true, returns the count of changed elements instead of new + * elements added. + */ + public GeoAddOptions(ConditionalChange updateMode, boolean changed) { + this.updateMode = updateMode; + this.changed = changed; + } + + /** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geoadd(String, Map, GeoAddOptions)} + * command. + * + * @param updateMode Options for handling existing members. See {@link ConditionalChange} + */ + public GeoAddOptions(ConditionalChange updateMode) { + this.updateMode = updateMode; + this.changed = false; + } + + /** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geoadd(String, Map, GeoAddOptions)} + * command. + * + * @param changed If true, returns the count of changed elements instead of new + * elements added. + */ + public GeoAddOptions(boolean changed) { + this.updateMode = null; + this.changed = changed; + } + + /** + * Converts GeoAddOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List arguments = new ArrayList<>(); + + if (updateMode != null) { + arguments.add(updateMode.getValkeyApi()); + } + + if (changed) { + arguments.add(CHANGED_VALKEY_API); + } + + return arguments.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java new file mode 100644 index 0000000000..24cc1be42a --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOptions.java @@ -0,0 +1,90 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +/** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geosearch(String, + * GeoSearchOrigin.SearchOrigin, GeoSearchShape, GeoSearchOptions)} command, options include: + * + *
    + *
  • WITHDIST: Also return the distance of the returned items from the specified center point. + * The distance is returned in the same unit as specified for the searchBy + * argument. + *
  • WITHCOORD: Also return the coordinate of the returned items. + *
  • WITHHASH: Also return the geohash of the returned items. for the general user. + *
+ * + * @see valkey.io + */ +@Builder +public final class GeoSearchOptions { + /** Valkey API keyword used to perform geosearch with coordinates. */ + public static final String WITHCOORD_VALKEY_API = "WITHCOORD"; + + /** Valkey API keyword used to perform geosearch with distance. */ + public static final String WITHDIST_VALKEY_API = "WITHDIST"; + + /** Valkey API keyword used to perform geosearch with hash value. */ + public static final String WITHHASH_VALKEY_API = "WITHHASH"; + + /** + * Indicates if the 'WITHCOORD' keyword should be included. Can be included in builder + * construction by using {@link GeoSearchOptionsBuilder#withcoord()}. + */ + @Builder.Default private boolean withCoord = false; + + /** + * Indicates if the 'WITHDIST' keyword should be included. Can be included in builder construction + * by using {@link GeoSearchOptionsBuilder#withdist()}. + */ + @Builder.Default private boolean withDist = false; + + /** + * Indicates if the 'WITHHASH' keyword should be included. Can be included in builder construction + * by using {@link GeoSearchOptionsBuilder#withhash()}. + */ + @Builder.Default private boolean withHash = false; + + /** + * Converts GeoSearchOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List arguments = new ArrayList<>(); + + if (withDist) { + arguments.add(WITHDIST_VALKEY_API); + } + + if (withCoord) { + arguments.add(WITHCOORD_VALKEY_API); + } + + if (withHash) { + arguments.add(WITHHASH_VALKEY_API); + } + + return arguments.toArray(new String[0]); + } + + public static class GeoSearchOptionsBuilder { + public GeoSearchOptionsBuilder() {} + + public GeoSearchOptionsBuilder withdist() { + return withDist(true); + } + + public GeoSearchOptionsBuilder withcoord() { + return withCoord(true); + } + + public GeoSearchOptionsBuilder withhash() { + return withHash(true); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java new file mode 100644 index 0000000000..9be1aa333a --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchOrigin.java @@ -0,0 +1,81 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import glide.api.models.GlideString; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.ArrayUtils; + +/** + * The query's starting point for {@link GeospatialIndicesBaseCommands} command. + * + * @see valkey.io + */ +public final class GeoSearchOrigin { + /** Valkey API keyword used to perform search from the position of a given member. */ + public static final String FROMMEMBER_VALKEY_API = "FROMMEMBER"; + + /** Valkey API keyword used to perform search from the given longtitude & latitue position. */ + public static final String FROMLONLAT_VALKEY_API = "FROMLONLAT"; + + /** + * Basic interface. Please use one of the following implementations: + * + *
    + *
  • {@link CoordOrigin} + *
  • {@link MemberOrigin} + *
+ */ + public interface SearchOrigin { + /** Convert to command arguments according to the Valkey API. */ + String[] toArgs(); + } + + /** The search origin represented by a {@link GeospatialData} position. */ + @RequiredArgsConstructor + public static class CoordOrigin implements SearchOrigin { + private final GeospatialData position; + + /** + * Converts GeoSearchOrigin into a String[]. + * + * @return String[] An array containing arguments corresponding to the starting point of the + * query. + */ + public String[] toArgs() { + return ArrayUtils.addAll(new String[] {FROMLONLAT_VALKEY_API}, position.toArgs()); + } + } + + /** The search origin represented by an existing member. */ + @RequiredArgsConstructor + public static class MemberOrigin implements SearchOrigin { + private final String member; + + /** + * Converts GeoSearchOrigin into a String[]. + * + * @return String[] An array containing arguments corresponding to the starting point of the + * query. + */ + public String[] toArgs() { + return new String[] {FROMMEMBER_VALKEY_API, member}; + } + } + + /** The search origin represented by an existing member. */ + @RequiredArgsConstructor + public static class MemberOriginBinary implements SearchOrigin { + private final GlideString member; + + /** + * Converts GeoSearchOrigin into a String[]. + * + * @return String[] An array containing arguments corresponding to the starting point of the + * query. + */ + public String[] toArgs() { + return new String[] {FROMMEMBER_VALKEY_API, member.toString()}; + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java new file mode 100644 index 0000000000..5a4e6c8b29 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchResultOptions.java @@ -0,0 +1,106 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import glide.api.models.commands.SortOrder; +import java.util.ArrayList; +import java.util.List; + +/** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geosearch(String, + * GeoSearchOrigin.SearchOrigin, GeoSearchShape, GeoSearchOptions)} command that contains up to 2 + * optional input, including: + * + *
    + *
  • SortOrder: The query's order to sort the results by: + *
      + *
    • ASC: Sort returned items from the nearest to the farthest, relative to the center + * point. + *
    • DESC: Sort returned items from the farthest to the nearest, relative to the center + * point. + *
    + *
  • COUNT: Limits the results to the first N matching items, when the 'ANY' option is used, the + * command returns as soon as enough matches are found. This means that the results might may + * not be the ones closest to the origin. + *
+ * + * @see valkey.io + */ +public class GeoSearchResultOptions { + /** Valkey API keyword used to perform geosearch with count. */ + public static final String COUNT_VALKEY_API = "COUNT"; + + /** + * Valkey API keyword used to change search behavior to return as soon as enough matches are + * found. + */ + public static final String ANY_VALKEY_API = "ANY"; + + /** Indicates the order the result should be sorted in. See {@link SortOrder} for detail. */ + private final SortOrder sortOrder; + + /** Indicates the number of matches the result should be limited to. */ + private final long count; + + /** Indicates if the 'ANY' keyword is used for 'COUNT'. */ + private final boolean isAny; + + /** Constructor with count only. */ + public GeoSearchResultOptions(long count) { + this.sortOrder = null; + this.count = count; + this.isAny = false; + } + + /** Constructor with count and the 'ANY' keyword. */ + public GeoSearchResultOptions(long count, boolean isAny) { + this.sortOrder = null; + this.count = count; + this.isAny = isAny; + } + + /** Constructor with sort order only. */ + public GeoSearchResultOptions(SortOrder order) { + this.sortOrder = order; + this.count = -1; + this.isAny = false; + } + + /** Constructor with sort order and count. */ + public GeoSearchResultOptions(SortOrder order, long count) { + this.sortOrder = order; + this.count = count; + this.isAny = false; + } + + /** Constructor with sort order, count and 'ANY' keyword. */ + public GeoSearchResultOptions(SortOrder order, long count, boolean isAny) { + this.sortOrder = order; + this.count = count; + this.isAny = isAny; + } + + /** + * Converts GeoSearchResultOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List arguments = new ArrayList<>(); + + if (count > 0) { + arguments.add(COUNT_VALKEY_API); + arguments.add(Long.toString(count)); + + if (isAny) { + arguments.add(ANY_VALKEY_API); + } + } + + if (sortOrder != null) { + arguments.add(sortOrder.toString()); + } + + return arguments.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java new file mode 100644 index 0000000000..3add2dadd9 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchShape.java @@ -0,0 +1,98 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import lombok.Getter; + +/** + * The query's shape for {@link GeospatialIndicesBaseCommands} command. + * + * @see valkey.io + */ +@Getter +public final class GeoSearchShape { + /** Valkey API keyword used to perform geosearch by radius. */ + public static final String BYRADIUS_VALKEY_API = "BYRADIUS"; + + /** Valkey API keyword used to perform geosearch by box. */ + public static final String BYBOX_VALKEY_API = "BYBOX"; + + /** + * The geosearch query's shape: + * + *
    + *
  • BYRADIUS - Circular shaped query. + *
  • BYBOX - Box shaped query. + *
+ */ + public enum SearchShape { + BYRADIUS, + BYBOX + } + + /** The geosearch query's shape. */ + private final SearchShape shape; + + /** The circular geosearch query's radius. */ + private final double radius; + + /** The box geosearch query's width. */ + private final double width; + + /** The box geosearch query's height. */ + private final double height; + + /** The geosearch query's metric unit. */ + private final GeoUnit unit; + + /** + * BYRADIUS constructor for GeoSearchShape + * + * @param radius The radius to search by. + * @param unit The unit of the radius. + */ + public GeoSearchShape(double radius, GeoUnit unit) { + this.shape = SearchShape.BYRADIUS; + this.radius = radius; + this.unit = unit; + + // unused variables + this.width = -1; + this.height = -1; + } + + /** + * BYBOX constructor for GeoSearchShape + * + * @param width The width to search by. + * @param height The height to search by. + * @param unit The unit of the radius. + */ + public GeoSearchShape(double width, double height, GeoUnit unit) { + this.shape = SearchShape.BYBOX; + this.width = width; + this.height = height; + this.unit = unit; + + // unused variable + this.radius = -1; + } + + /** + * Converts GeoSearchShape into a String[]. + * + * @return String[] An array containing arguments corresponding to the shape to search by. + */ + public String[] toArgs() { + switch (shape) { + case BYRADIUS: + return new String[] {shape.toString(), Double.toString(radius), unit.getValkeyAPI()}; + case BYBOX: + return new String[] { + shape.toString(), Double.toString(width), Double.toString(height), unit.getValkeyAPI() + }; + default: + return new String[] {}; + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java new file mode 100644 index 0000000000..ec2c50d34e --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java @@ -0,0 +1,47 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import lombok.Builder; + +/** + * Optional arguments for {@link GeospatialIndicesBaseCommands#geosearchstore(String, String, + * GeoSearchOrigin.SearchOrigin, GeoSearchShape, GeoSearchStoreOptions)} command. + * + * @see valkey.io + */ +@Builder +public final class GeoSearchStoreOptions { + /** + * Valkey API keyword used to perform geosearchstore and optionally sort the results with their + * distance from the center. + */ + public static final String GEOSEARCHSTORE_VALKEY_API = "STOREDIST"; + + /** + * boolean value indicating if the STOREDIST option should be included. Can be included in builder + * construction by using {@link GeoSearchStoreOptionsBuilder#storedist()}. + */ + private final boolean storeDist; + + /** + * Converts GeoSearchStoreOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + if (storeDist) { + return new String[] {GEOSEARCHSTORE_VALKEY_API}; + } + + return new String[] {}; + } + + public static class GeoSearchStoreOptionsBuilder { + public GeoSearchStoreOptionsBuilder() {} + + public GeoSearchStoreOptionsBuilder storedist() { + return storeDist(true); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java new file mode 100644 index 0000000000..b81bcecdeb --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoUnit.java @@ -0,0 +1,25 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Enumeration representing distance units options for the {@link + * GeospatialIndicesBaseCommands#geodist(String, String, String, GeoUnit)} command. + */ +@RequiredArgsConstructor +@Getter +public enum GeoUnit { + /** Represents distance in meters. */ + METERS("m"), + /** Represents distance in kilometers. */ + KILOMETERS("km"), + /** Represents distance in miles. */ + MILES("mi"), + /** Represents distance in feet. */ + FEET("ft"); + + private final String valkeyAPI; +} diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeospatialData.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeospatialData.java new file mode 100644 index 0000000000..a72df6a8c1 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeospatialData.java @@ -0,0 +1,34 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.geospatial; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents a geographic position defined by longitude and latitude.
+ * The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 are the + * following: + * + *
    + *
  • Valid longitudes are from -180 to 180 degrees. + *
  • Valid latitudes are from -85.05112878 to 85.05112878 degrees. + *
+ */ +@Getter +@RequiredArgsConstructor +public final class GeospatialData { + /** The longitude coordinate. */ + private final double longitude; + + /** The latitude coordinate. */ + private final double latitude; + + /** + * Converts GeospatialData into a String[]. + * + * @return String[] An array containing the longtitue and the latitue of the position. + */ + public String[] toArgs() { + return new String[] {Double.toString(longitude), Double.toString(latitude)}; + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/BaseScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/BaseScanOptions.java new file mode 100644 index 0000000000..2bc705bccd --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/BaseScanOptions.java @@ -0,0 +1,86 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.models.GlideString; +import glide.utils.ArgsBuilder; +import java.util.Arrays; +import java.util.Objects; +import lombok.experimental.SuperBuilder; + +/** + * This base class represents the common set of optional arguments for the SCAN family of commands. + * Concrete implementations of this class are tied to specific SCAN commands (SCAN, HSCAN, SSCAN, + * and ZSCAN). + */ +@SuperBuilder +public abstract class BaseScanOptions { + /** MATCH option string to include in the SCAN commands. */ + public static final String MATCH_OPTION_STRING = "MATCH"; + + /** COUNT option string to include in the SCAN commands. */ + public static final String COUNT_OPTION_STRING = "COUNT"; + + /** + * The match filter is applied to the result of the command and will only include strings that + * match the pattern specified. If the set, hash, or list is large enough for scan commands to + * return only a subset of the set, hash, or list, then there could be a case where the result is + * empty although there are items that match the pattern specified. This is due to the default + * COUNT being 10 which indicates that it will only fetch and match + * 10 items from the list. + */ + protected final String matchPattern; + + /** + * The match filter is applied to the result of the command and will only include strings that + * match the pattern specified. If the set, hash, or list is large enough for scan commands to + * return only a subset of the set, hash, or list, then there could be a case where the result is + * empty although there are items that match the pattern specified. This is due to the default + * COUNT being 10 which indicates that it will only fetch and match + * 10 items from the list. + */ + protected final GlideString matchPatternBinary; + + /** + * COUNT is a just a hint for the command for how many elements to fetch from the + * set, hash, or list. COUNT could be ignored until the set, hash, or list is large + * enough for the SCAN commands to represent the results as compact single-allocation + * packed encoding. + */ + protected final Long count; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BaseScanOptions)) return false; + BaseScanOptions that = (BaseScanOptions) o; + return Objects.equals(matchPattern, that.matchPattern) && Objects.equals(count, that.count); + } + + /** + * Creates the arguments to be used in SCAN commands. + * + * @return a String array that holds the options and their arguments. + */ + public String[] toArgs() { + return Arrays.stream(toGlideStringArgs()).map(e -> e.getString()).toArray(String[]::new); + } + + /** + * Creates the arguments to be used in SCAN commands. + * + * @return a String array that holds the options and their arguments. + */ + public GlideString[] toGlideStringArgs() { + ArgsBuilder builder = new ArgsBuilder(); + builder.addIf(MATCH_OPTION_STRING, (matchPattern != null)); + // Add the pattern. We prefer the binary version first + if (matchPatternBinary != null) { + builder.add(matchPatternBinary); + } else if (matchPattern != null) { + builder.add(matchPattern); + } + builder.addIf(COUNT_OPTION_STRING, (count != null)); + builder.addIf(count, (count != null)); + return builder.toArray(); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/BaseScanOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/scan/BaseScanOptionsBinary.java new file mode 100644 index 0000000000..98d880e220 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/BaseScanOptionsBinary.java @@ -0,0 +1,62 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import static glide.api.models.GlideString.gs; + +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import lombok.experimental.SuperBuilder; + +/** + * This base class represents the common set of optional arguments for the SCAN family of commands. + * Concrete implementations of this class are tied to specific SCAN commands (SCAN, HSCAN, SSCAN, + * and ZSCAN). + */ +@SuperBuilder +public abstract class BaseScanOptionsBinary { + /** MATCH option string to include in the SCAN commands. */ + public static final GlideString MATCH_OPTION_GLIDE_STRING = gs("MATCH"); + + /** COUNT option string to include in the SCAN commands. */ + public static final GlideString COUNT_OPTION_GLIDE_STRING = gs("COUNT"); + + /** + * The match filter is applied to the result of the command and will only include strings that + * match the pattern specified. If the set, hash, or list is large enough for scan commands to + * return only a subset of the set, hash, or list, then there could be a case where the result is + * empty although there are items that match the pattern specified. This is due to the default + * COUNT being 10 which indicates that it will only fetch and match + * 10 items from the list. + */ + private final GlideString matchPattern; + + /** + * COUNT is a just a hint for the command for how many elements to fetch from the + * set, hash, or list. COUNT could be ignored until the set, hash, or list is large + * enough for the SCAN commands to represent the results as compact single-allocation + * packed encoding. + */ + private final Long count; + + /** + * Creates the arguments to be used in SCAN commands. + * + * @return a String array that holds the options and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (matchPattern != null) { + optionArgs.add(MATCH_OPTION_GLIDE_STRING.toString()); + optionArgs.add(matchPattern.toString()); + } + + if (count != null) { + optionArgs.add(COUNT_OPTION_GLIDE_STRING.toString()); + optionArgs.add(count.toString()); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java b/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java new file mode 100644 index 0000000000..bc3101ae71 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/ClusterScanCursor.java @@ -0,0 +1,92 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.GenericClusterCommands; + +/** + * A cursor is used to iterate through data returned by cluster SCAN requests. + * + *

This interface is used in two ways: + * + *

    + *
  1. An {@link #initalCursor()} is passed to {@link GenericClusterCommands#scan} to start a + * cluster SCAN request. + *
  2. The result of the {@link GenericClusterCommands#scan} call returns a cursor at index + * 0 of the returned Object[]. This cursor can be supplied again to a call + * to {@link GenericClusterCommands#scan}, provided that {@link #isFinished()} returns + * false. + *
+ * + *

Note that cursors returned by {@link GenericClusterCommands#scan} may hold external resources. + * These resources can be released by calling {@link #releaseCursorHandle()}. However, doing so will + * invalidate the cursor from being used in another {@link GenericClusterCommands#scan}. + * + *

To do this safely, follow this procedure: + * + *

    + *
  1. Call {@link GenericClusterCommands#scan} with the cursor. + *
  2. Call {@link #releaseCursorHandle()} to destroy the cursor. + *
  3. Assign the new cursor returned by {@link GenericClusterCommands#scan}. + *
+ * + * @see GenericClusterCommands#scan + * @example + *
{@code
+ * ClusterScanCursor cursor = ClusterScanCursor.initialCursor();
+ * Object[] result;
+ * while (!cursor.isFinished()) {
+ *     result = client.scan(cursor).get();
+ *     cursor.releaseCursorHandle();
+ *     cursor = (ClusterScanCursor) result[0];
+ *     Object[] stringResults = (Object[]) result[1];
+ *
+ *     System.out.println("\nSCAN iteration:");
+ *     Arrays.asList(stringResults).stream().forEach(i -> System.out.print(i + ", "));
+ * }
+ * }
+ */ +public interface ClusterScanCursor { + /** + * Indicates if this cursor is the last set of data available. + * + *

If this method returns false, this cursor instance should get passed to {@link + * GenericClusterCommands#scan} to get next set of data. + * + * @return true if this cursor is the last set of data. False if there is potentially more data + * available. + */ + boolean isFinished(); + + /** + * Releases resources related to this cursor. + * + *

This method can be called to immediately release resources tied to this cursor instance. + * Note that if this is called, this cursor cannot be used in {@link GenericClusterCommands#scan}. + * Also, this method is optional for the caller. If it does not get called, cursor resources will + * be freed during garbage collection. + */ + void releaseCursorHandle(); + + /** + * The special cursor used to start the first in a series of {@link GenericClusterCommands#scan} + * calls. + */ + ClusterScanCursor INITIAL_CURSOR_INSTANCE = + new ClusterScanCursor() { + @Override + public boolean isFinished() { + // The initial cursor can always request more data. + return false; + } + + @Override + public void releaseCursorHandle() { + // Ignore. + } + }; + + /** Creates an empty cursor to be used in the initial {@link GenericClusterCommands#scan} call. */ + static ClusterScanCursor initalCursor() { + return INITIAL_CURSOR_INSTANCE; + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/HScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/HScanOptions.java new file mode 100644 index 0000000000..1f03a00e6d --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/HScanOptions.java @@ -0,0 +1,13 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.HashBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link HashBaseCommands#hscan(String, String, HScanOptions)}. + * + * @see valkey.io + */ +@SuperBuilder +public class HScanOptions extends BaseScanOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/HScanOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/scan/HScanOptionsBinary.java new file mode 100644 index 0000000000..b68506c9c4 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/HScanOptionsBinary.java @@ -0,0 +1,14 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.HashBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link HashBaseCommands#hscan(GlideString, GlideString, + * HScanOptionsBinary)}. + * + * @see valkey.io + */ +@SuperBuilder +public class HScanOptionsBinary extends BaseScanOptionsBinary {} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/SScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/SScanOptions.java new file mode 100644 index 0000000000..ecea070450 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/SScanOptions.java @@ -0,0 +1,13 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.SetBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link SetBaseCommands#sscan(String, String, SScanOptions)}. + * + * @see valkey.io + */ +@SuperBuilder +public class SScanOptions extends BaseScanOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/SScanOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/scan/SScanOptionsBinary.java new file mode 100644 index 0000000000..154f4ef3b8 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/SScanOptionsBinary.java @@ -0,0 +1,14 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.SetBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link SetBaseCommands#sscan(GlideString, GlideString, + * SScanOptionsBinary)}. + * + * @see valkey.io + */ +@SuperBuilder +public class SScanOptionsBinary extends BaseScanOptionsBinary {} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java new file mode 100644 index 0000000000..6fffc46f35 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/ScanOptions.java @@ -0,0 +1,89 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.GenericClusterCommands; +import glide.api.commands.GenericCommands; +import glide.api.models.GlideString; +import glide.ffi.resolvers.ObjectTypeResolver; +import glide.utils.ArrayTransformUtils; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link GenericCommands#scan} and {@link GenericClusterCommands#scan}. + * + * @see valkey.io + */ +@SuperBuilder +@EqualsAndHashCode +public class ScanOptions extends BaseScanOptions { + /** TYPE option string to include in the SCAN commands. */ + public static final String TYPE_OPTION_STRING = "TYPE"; + + /** + * Use this option to ask SCAN to only return objects that match a given type.
+ * The filter is applied after elements are retrieved from the database, so the option does not + * reduce the amount of work the server has to do to complete a full iteration. For rare types you + * may receive no elements in many iterations. + */ + private final ObjectType type; + + /** Defines the complex data types available for a SCAN request. */ + public enum ObjectType { + STRING(ObjectTypeResolver.OBJECT_TYPE_STRING_NATIVE_NAME), + LIST(ObjectTypeResolver.OBJECT_TYPE_LIST_NATIVE_NAME), + SET(ObjectTypeResolver.OBJECT_TYPE_SET_NATIVE_NAME), + ZSET(ObjectTypeResolver.OBJECT_TYPE_ZSET_NATIVE_NAME), + HASH(ObjectTypeResolver.OBJECT_TYPE_HASH_NATIVE_NAME), + STREAM(ObjectTypeResolver.OBJECT_TYPE_STREAM_NATIVE_NAME); + + /** + * @return the name of the enum when communicating with the native layer. + */ + public String getNativeName() { + return nativeName; + } + + private final String nativeName; + + ObjectType(String nativeName) { + this.nativeName = nativeName; + } + } + + @Override + public String[] toArgs() { + if (type != null) { + return ArrayTransformUtils.concatenateArrays( + super.toArgs(), new String[] {TYPE_OPTION_STRING, type.name()}); + } + return super.toArgs(); + } + + /** + * @return the pattern used for the MATCH filter. + */ + public GlideString getMatchPattern() { + if (matchPatternBinary != null) { + return matchPatternBinary; + } else if (matchPattern != null) { + return GlideString.of(matchPattern); + } else { + return null; + } + } + + /** + * @return the count used for the COUNT field. + */ + public Long getCount() { + return count; + } + + /** + * @return the type used for the TYPE filter. + */ + public ObjectType getType() { + return type; + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java b/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java new file mode 100644 index 0000000000..dbda89106b --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptions.java @@ -0,0 +1,13 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.SortedSetBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link SortedSetBaseCommands#zscan(String, String, ZScanOptions)}. + * + * @see valkey.io + */ +@SuperBuilder +public class ZScanOptions extends BaseScanOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptionsBinary.java new file mode 100644 index 0000000000..288c75e4e7 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/scan/ZScanOptionsBinary.java @@ -0,0 +1,14 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.scan; + +import glide.api.commands.SortedSetBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link SortedSetBaseCommands#zscan(GlideString, GlideString, + * ZScanOptionsBinary)}. + * + * @see valkey.io + */ +@SuperBuilder +public class ZScanOptionsBinary extends BaseScanOptionsBinary {} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java new file mode 100644 index 0000000000..8d7918ed83 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptions.java @@ -0,0 +1,57 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.commands.StreamBaseCommands; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.Builder; + +/** + * Optional arguments to {@link StreamBaseCommands#xadd(String, Map, StreamAddOptions)} + * + * @see valkey.io + */ +@Builder +public final class StreamAddOptions { + public static final String NO_MAKE_STREAM_VALKEY_API = "NOMKSTREAM"; + public static final String ID_WILDCARD_VALKEY_API = "*"; + + /** If set, the new entry will be added with this id. */ + private final String id; + + /** + * If set to false, a new stream won't be created if no stream matches the given key. + *
+ * Equivalent to NOMKSTREAM in the Valkey API. + */ + private final Boolean makeStream; + + /** If set, the add operation will also trim the older entries in the stream. */ + private final StreamTrimOptions trim; + + /** + * Converts options for Xadd into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (makeStream != null && !makeStream) { + optionArgs.add(NO_MAKE_STREAM_VALKEY_API); + } + + if (trim != null) { + optionArgs.addAll(trim.getValkeyApi()); + } + + if (id != null) { + optionArgs.add(id); + } else { + optionArgs.add(ID_WILDCARD_VALKEY_API); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptionsBinary.java new file mode 100644 index 0000000000..ed14e6862d --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamAddOptionsBinary.java @@ -0,0 +1,62 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import static glide.api.models.GlideString.gs; + +import glide.api.commands.StreamBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Builder; + +/** + * Optional arguments to {@link StreamBaseCommands#xadd(GlideString, Map, StreamAddOptionsBinary)} + * + * @see valkey.io + */ +@Builder +public final class StreamAddOptionsBinary { + public static final GlideString NO_MAKE_STREAM_VALKEY_API_GLIDE_STRING = gs("NOMKSTREAM"); + public static final GlideString ID_WILDCARD_VALKEY_API_GLIDE_STRING = gs("*"); + + /** If set, the new entry will be added with this id. */ + private final GlideString id; + + /** + * If set to false, a new stream won't be created if no stream matches the given key. + *
+ * Equivalent to NOMKSTREAM in the Valkey API. + */ + private final Boolean makeStream; + + /** If set, the add operation will also trim the older entries in the stream. */ + private final StreamTrimOptions trim; + + /** + * Converts options for Xadd into a GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (makeStream != null && !makeStream) { + optionArgs.add(NO_MAKE_STREAM_VALKEY_API_GLIDE_STRING); + } + + if (trim != null) { + optionArgs.addAll( + trim.getValkeyApi().stream().map(GlideString::gs).collect(Collectors.toList())); + } + + if (id != null) { + optionArgs.add(id); + } else { + optionArgs.add(ID_WILDCARD_VALKEY_API_GLIDE_STRING); + } + + return optionArgs.toArray(new GlideString[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamClaimOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamClaimOptions.java new file mode 100644 index 0000000000..4b584d13a8 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamClaimOptions.java @@ -0,0 +1,104 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.commands.StreamBaseCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +/** + * Optional arguments to {@link StreamBaseCommands#xclaim(String, String, String, long, String[], + * StreamClaimOptions)} + * + * @see valkey.io + */ +@Builder +public class StreamClaimOptions { + + /** ValKey API string to designate IDLE time in milliseconds */ + public static final String IDLE_VALKEY_API = "IDLE"; + + /** ValKey API string to designate TIME time in unix-milliseconds */ + public static final String TIME_VALKEY_API = "TIME"; + + /** ValKey API string to designate RETRYCOUNT */ + public static final String RETRY_COUNT_VALKEY_API = "RETRYCOUNT"; + + /** ValKey API string to designate FORCE */ + public static final String FORCE_VALKEY_API = "FORCE"; + + /** ValKey API string to designate JUSTID */ + public static final String JUST_ID_VALKEY_API = "JUSTID"; + + /** + * Set the idle time (last time it was delivered) of the message in milliseconds. If idle + * is not specified, an idle of 0 is assumed, that is, the time + * count is reset because the message now has a new owner trying to process it. + */ + private final Long idle; // in milliseconds + + /** + * This is the same as {@link #idle} but instead of a relative amount of milliseconds, it sets the + * idle time to a specific Unix time (in milliseconds). This is useful in order to rewrite the AOF + * file generating XCLAIM commands. + */ + private final Long idleUnixTime; // in unix-time milliseconds + + /** + * Set the retry counter to the specified value. This counter is incremented every time a message + * is delivered again. Normally {@link StreamBaseCommands#xclaim} does not alter this counter, + * which is just served to clients when the {@link StreamBaseCommands#xpending} command is called: + * this way clients can detect anomalies, like messages that are never processed for some reason + * after a big number of delivery attempts. + */ + private final Long retryCount; + + /** + * Creates the pending message entry in the PEL even if certain specified IDs are not already in + * the PEL assigned to a different client. However, the message must exist in the stream, + * otherwise the IDs of non-existing messages are ignored. + */ + private final boolean isForce; + + public static class StreamClaimOptionsBuilder { + + /** + * Creates the pending message entry in the PEL even if certain specified IDs are not already in + * the PEL assigned to a different client. However, the message must exist in the stream, + * otherwise the IDs of non-existing messages are ignored. + */ + public StreamClaimOptionsBuilder force() { + return isForce(true); + } + } + + /** + * Converts options for Xclaim into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (idle != null) { + optionArgs.add(IDLE_VALKEY_API); + optionArgs.add(Long.toString(idle)); + } + + if (idleUnixTime != null) { + optionArgs.add(TIME_VALKEY_API); + optionArgs.add(Long.toString(idleUnixTime)); + } + + if (retryCount != null) { + optionArgs.add(RETRY_COUNT_VALKEY_API); + optionArgs.add(Long.toString(retryCount)); + } + + if (isForce) { + optionArgs.add(FORCE_VALKEY_API); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamGroupOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamGroupOptions.java new file mode 100644 index 0000000000..5943c2fd55 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamGroupOptions.java @@ -0,0 +1,65 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.commands.StreamBaseCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +/** + * Optional arguments for {@link StreamBaseCommands#xgroupCreate(String, String, String, + * StreamGroupOptions)} + * + * @see valkey.io + */ +@Builder +public final class StreamGroupOptions { + + // Valkey API String argument for makeStream + public static final String MAKE_STREAM_VALKEY_API = "MKSTREAM"; + + // Valkey API String argument for entriesRead + public static final String ENTRIES_READ_VALKEY_API = "ENTRIESREAD"; + + /** + * If true and the stream doesn't exist, creates a new stream with a length of + * 0. + */ + @Builder.Default private boolean mkStream = false; + + public static class StreamGroupOptionsBuilder { + + /** If the stream doesn't exist, this creates a new stream with a length of 0. */ + public StreamGroupOptionsBuilder makeStream() { + return mkStream(true); + } + } + + /** + * A value representing the number of stream entries already read by the group. + * + * @since Valkey 7.0.0 + */ + private Long entriesRead; + + /** + * Converts options and the key-to-id input for {@link StreamBaseCommands#xgroupCreate(String, + * String, String, StreamGroupOptions)} into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (this.mkStream) { + optionArgs.add(MAKE_STREAM_VALKEY_API); + } + + if (this.entriesRead != null) { + optionArgs.add(ENTRIES_READ_VALKEY_API); + optionArgs.add(entriesRead.toString()); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamPendingOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamPendingOptions.java new file mode 100644 index 0000000000..af28968deb --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamPendingOptions.java @@ -0,0 +1,49 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.commands.StreamBaseCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +/** + * Arguments for {@link StreamBaseCommands#xpending} to specify additional filter items by idle time + * and consumer. + * + * @see valkey.io + */ +@Builder +public class StreamPendingOptions { + + /** Valkey api string to designate IDLE or minimum idle time */ + public static final String IDLE_TIME_VALKEY_API = "IDLE"; + + /** Filters pending entries by their idle time - in Milliseconds */ + private final Long minIdleTime; // Milliseconds + + /** Filters pending entries by consumer */ + private final String consumer; + + /** + * Convert StreamPendingOptions arguments to a string array + * + * @return arguments converted to an array to be consumed by Valkey. + */ + public String[] toArgs(StreamRange start, StreamRange end, long count) { + List optionArgs = new ArrayList<>(); + if (minIdleTime != null) { + optionArgs.add(IDLE_TIME_VALKEY_API); + optionArgs.add(Long.toString(minIdleTime)); + } + + optionArgs.add(start.getValkeyApi()); + optionArgs.add(end.getValkeyApi()); + optionArgs.add(Long.toString(count)); + + if (consumer != null) { + optionArgs.add(consumer); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamPendingOptionsBinary.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamPendingOptionsBinary.java new file mode 100644 index 0000000000..2f7772a64f --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamPendingOptionsBinary.java @@ -0,0 +1,52 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import static glide.api.models.GlideString.gs; + +import glide.api.commands.StreamBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; + +/** + * Arguments for {@link StreamBaseCommands#xpending} to specify additional filter items by idle time + * and consumer. + * + * @see valkey.io + */ +@Builder +public class StreamPendingOptionsBinary { + + /** Valkey api string to designate IDLE or minimum idle time */ + public static final GlideString IDLE_TIME_VALKEY_API_GLIDE_STRING = gs("IDLE"); + + /** Filters pending entries by their idle time - in Milliseconds */ + private final Long minIdleTime; // Milliseconds + + /** Filters pending entries by consumer */ + private final GlideString consumer; + + /** + * Convert StreamPendingOptions arguments to a string array + * + * @return arguments converted to an array to be consumed by Valkey + */ + public GlideString[] toArgs(StreamRange start, StreamRange end, long count) { + List optionArgs = new ArrayList<>(); + if (minIdleTime != null) { + optionArgs.add(IDLE_TIME_VALKEY_API_GLIDE_STRING); + optionArgs.add(gs(Long.toString(minIdleTime))); + } + + optionArgs.add(gs(start.getValkeyApi())); + optionArgs.add(gs(end.getValkeyApi())); + optionArgs.add(gs(Long.toString(count))); + + if (consumer != null) { + optionArgs.add(consumer); + } + + return optionArgs.toArray(new GlideString[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamRange.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamRange.java new file mode 100644 index 0000000000..7044a56dd5 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamRange.java @@ -0,0 +1,150 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.models.GlideString; +import glide.utils.ArrayTransformUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Arguments for {@link glide.api.commands.StreamBaseCommands#xrange} and {@link + * glide.api.commands.StreamBaseCommands#xrevrange} to specify the starting and ending range for the + * stream search by stream ID. + * + * @see valkey.io + * @see valkey.io + */ +public interface StreamRange { + + String getValkeyApi(); + + /** Valkey API string for MINIMUM entry ID range bounds */ + String MINIMUM_RANGE_VALKEY_API = "-"; + + /** Valkey API string for MAXIMUM entry ID range bounds */ + String MAXIMUM_RANGE_VALKEY_API = "+"; + + /** Valkey API string to designate COUNT */ + String RANGE_COUNT_VALKEY_API = "COUNT"; + + /** Valkey API character to designate exclusive range bounds */ + String EXCLUSIVE_RANGE_VALKEY_API = "("; + + /** + * Enumeration representing minimum or maximum stream entry bounds for the range search, to get + * the first or last stream ID. + */ + @RequiredArgsConstructor + @Getter + enum InfRangeBound implements StreamRange { + MIN(MINIMUM_RANGE_VALKEY_API), + MAX(MAXIMUM_RANGE_VALKEY_API); + + private final String valkeyApi; + }; + + /** + * Stream ID used to specify a range of IDs to search. Stream ID bounds can be complete with a + * timestamp and sequence number separated by a dash ("-"), for example + * "1526985054069-0".
+ * Stream ID bounds can also be incomplete, with just a timestamp.
+ * Stream ID bounds are inclusive by default. When isInclusive==false, a "(" + * is prepended for the Valkey API. + */ + @Getter + class IdBound implements StreamRange { + private final String valkeyApi; + + /** + * Default constructor + * + * @param id The stream id. + */ + private IdBound(String id) { + valkeyApi = id; + } + + /** + * Default constructor + * + * @param id The stream id. + */ + private IdBound(GlideString id) { + valkeyApi = id.getString(); + } + + /** + * Creates a stream ID boundary by stream id for range search. + * + * @param id The stream id. + */ + public static IdBound of(String id) { + return new IdBound(id); + } + + /** + * Creates a stream ID boundary by stream id for range search. + * + * @param id The stream id. + */ + public static IdBound of(GlideString id) { + return new IdBound(id); + } + + /** + * Creates an incomplete stream ID boundary without the sequence number for range search. + * + * @param timestamp The stream timestamp as ID. + */ + public static IdBound of(long timestamp) { + return new IdBound(Long.toString(timestamp)); + } + + /** + * Creates an incomplete stream ID exclusive boundary without the sequence number for range + * search. + * + * @param timestamp The stream timestamp as ID. + */ + public static IdBound ofExclusive(long timestamp) { + return new IdBound(EXCLUSIVE_RANGE_VALKEY_API + timestamp); + } + + /** + * Creates a stream ID exclusive boundary by stream id for range search. + * + * @param id The stream id. + */ + public static IdBound ofExclusive(String id) { + return new IdBound(EXCLUSIVE_RANGE_VALKEY_API + id); + } + + /** + * Creates a stream ID exclusive boundary by stream id for range search. + * + * @param id The stream id. + */ + public static IdBound ofExclusive(GlideString id) { + return new IdBound(EXCLUSIVE_RANGE_VALKEY_API + id.getString()); + } + } + + /** + * Convert StreamRange arguments to a string array + * + * @return arguments converted to an array to be consumed by Valkey. + */ + static String[] toArgs(StreamRange start, StreamRange end) { + return new String[] {start.getValkeyApi(), end.getValkeyApi()}; + } + + /** + * Convert StreamRange arguments to a string array + * + * @return arguments converted to an array to be consumed by Valkey. + */ + static String[] toArgs(StreamRange start, StreamRange end, long count) { + return ArrayTransformUtils.concatenateArrays( + toArgs(start, end), new String[] {RANGE_COUNT_VALKEY_API, Long.toString(count)}); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamReadGroupOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamReadGroupOptions.java new file mode 100644 index 0000000000..3fa6a9ce52 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamReadGroupOptions.java @@ -0,0 +1,141 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import static glide.api.models.GlideString.gs; + +import glide.api.commands.StreamBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link StreamBaseCommands#xreadgroup(Map, String, String, + * StreamReadGroupOptions)} and {@link StreamBaseCommands#xreadgroup(Map, GlideString, GlideString, + * StreamReadGroupOptions)} + * + * @see valkey.io + */ +@SuperBuilder +public final class StreamReadGroupOptions extends StreamReadOptions { + + public static final String READ_GROUP_VALKEY_API = "GROUP"; + public static final String READ_NOACK_VALKEY_API = "NOACK"; + + /** + * If set, messages are not added to the Pending Entries List (PEL). This is equivalent to + * acknowledging the message when it is read. + */ + private boolean noack; + + public abstract static class StreamReadGroupOptionsBuilder< + C extends StreamReadGroupOptions, B extends StreamReadGroupOptionsBuilder> + extends StreamReadOptions.StreamReadOptionsBuilder { + public B noack() { + this.noack = true; + return self(); + } + } + + /** + * Converts options and the key-to-id input for {@link StreamBaseCommands#xreadgroup(Map, ArgType, + * ArgType, StreamReadGroupOptions)} into a GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toArgs(ArgType group, ArgType consumer) { + List optionArgs = new ArrayList<>(); + optionArgs.add(GlideString.of(READ_GROUP_VALKEY_API)); + optionArgs.add(GlideString.of(group)); + optionArgs.add(GlideString.of(consumer)); + + if (this.count != null) { + optionArgs.add(GlideString.of(READ_COUNT_VALKEY_API)); + optionArgs.add(GlideString.of(count.toString())); + } + + if (this.block != null) { + optionArgs.add(GlideString.of(READ_BLOCK_VALKEY_API)); + optionArgs.add(GlideString.of(block.toString())); + } + + if (this.noack) { + optionArgs.add(GlideString.of(READ_NOACK_VALKEY_API)); + } + + optionArgs.add(GlideString.of(READ_STREAMS_VALKEY_API)); + return optionArgs.toArray(new GlideString[0]); + } + + /** + * Converts options and the key-to-id input for {@link StreamBaseCommands#xreadgroup(Map, String, + * String, StreamReadGroupOptions)} into a String[]. + * + * @return String[] + */ + public String[] toArgs(String group, String consumer, Map streams) { + List optionArgs = new ArrayList<>(); + optionArgs.add(READ_GROUP_VALKEY_API); + optionArgs.add(group); + optionArgs.add(consumer); + + if (this.count != null) { + optionArgs.add(READ_COUNT_VALKEY_API); + optionArgs.add(count.toString()); + } + + if (this.block != null) { + optionArgs.add(READ_BLOCK_VALKEY_API); + optionArgs.add(block.toString()); + } + + if (this.noack) { + optionArgs.add(READ_NOACK_VALKEY_API); + } + + optionArgs.add(READ_STREAMS_VALKEY_API); + Set> entrySet = streams.entrySet(); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getKey).collect(Collectors.toList())); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getValue).collect(Collectors.toList())); + + return optionArgs.toArray(new String[0]); + } + + /** + * Converts options and the key-to-id input for {@link StreamBaseCommands#xreadgroupBinary(Map, + * GlideString, GlideString, StreamReadGroupOptions)} into a GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toArgsBinary( + GlideString group, GlideString consumer, Map streams) { + List optionArgs = new ArrayList<>(); + optionArgs.add(gs(READ_GROUP_VALKEY_API)); + optionArgs.add(group); + optionArgs.add(consumer); + + if (this.count != null) { + optionArgs.add(gs(READ_COUNT_VALKEY_API)); + optionArgs.add(gs(count.toString())); + } + + if (this.block != null) { + optionArgs.add(gs(READ_BLOCK_VALKEY_API)); + optionArgs.add(gs(block.toString())); + } + + if (this.noack) { + optionArgs.add(gs(READ_NOACK_VALKEY_API)); + } + + optionArgs.add(gs(READ_STREAMS_VALKEY_API)); + Set> entrySet = streams.entrySet(); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getKey).collect(Collectors.toList())); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getValue).collect(Collectors.toList())); + + return optionArgs.toArray(new GlideString[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamReadOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamReadOptions.java new file mode 100644 index 0000000000..6fdedbba92 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamReadOptions.java @@ -0,0 +1,112 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import static glide.api.models.GlideString.gs; + +import glide.api.commands.StreamBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments for {@link StreamBaseCommands#xread(Map, StreamReadOptions)} + * + * @see valkey.io + */ +@SuperBuilder +public class StreamReadOptions { + + public static final String READ_COUNT_VALKEY_API = "COUNT"; + public static final String READ_BLOCK_VALKEY_API = "BLOCK"; + public static final String READ_STREAMS_VALKEY_API = "STREAMS"; + + /** + * If set, the request will be blocked for the set amount of milliseconds or until the server has + * the required number of entries. Equivalent to BLOCK in the Valkey API. + */ + protected Long block; + + /** + * The maximal number of elements requested. Equivalent to COUNT in the Valkey API. + */ + protected Long count; + + /** + * Converts options and the key-to-id input for {@link StreamBaseCommands#xread(Map, + * StreamReadOptions)} into a String[]. + * + * @return String[] + */ + public String[] toArgs(Map streams) { + List optionArgs = new ArrayList<>(); + + if (this.count != null) { + optionArgs.add(READ_COUNT_VALKEY_API); + optionArgs.add(count.toString()); + } + + if (this.block != null) { + optionArgs.add(READ_BLOCK_VALKEY_API); + optionArgs.add(block.toString()); + } + + optionArgs.add(READ_STREAMS_VALKEY_API); + Set> entrySet = streams.entrySet(); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getKey).collect(Collectors.toList())); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getValue).collect(Collectors.toList())); + + return optionArgs.toArray(new String[0]); + } + + /** + * Converts options and the key-to-id input for {@link StreamBaseCommands#xread(Map, + * StreamReadOptions)} into a GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toArgsBinary(Map streams) { + List optionArgs = new ArrayList<>(); + + if (this.count != null) { + optionArgs.add(gs(READ_COUNT_VALKEY_API)); + optionArgs.add(gs(count.toString())); + } + + if (this.block != null) { + optionArgs.add(gs(READ_BLOCK_VALKEY_API)); + optionArgs.add(gs(block.toString())); + } + + optionArgs.add(gs(READ_STREAMS_VALKEY_API)); + Set> entrySet = streams.entrySet(); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getKey).collect(Collectors.toList())); + optionArgs.addAll(entrySet.stream().map(Map.Entry::getValue).collect(Collectors.toList())); + + return optionArgs.toArray(new GlideString[0]); + } + + /** + * Converts options into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + Map emptyMap = new HashMap<>(); + return toArgs(emptyMap); + } + + /** + * Converts options into a GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toArgsBinary() { + Map emptyMap = new HashMap<>(); + return toArgsBinary(emptyMap); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/StreamTrimOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/StreamTrimOptions.java new file mode 100644 index 0000000000..e57ec27a6f --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/StreamTrimOptions.java @@ -0,0 +1,212 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.commands.StreamBaseCommands; +import glide.api.models.GlideString; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.NonNull; + +/** + * Optional arguments for {@link StreamBaseCommands#xtrim(String, StreamTrimOptions)} + * + * @see valkey.io + */ +public abstract class StreamTrimOptions { + + public static final String TRIM_MAXLEN_VALKEY_API = "MAXLEN"; + public static final String TRIM_MINID_VALKEY_API = "MINID"; + public static final String TRIM_EXACT_VALKEY_API = "="; + public static final String TRIM_NOT_EXACT_VALKEY_API = "~"; + public static final String TRIM_LIMIT_VALKEY_API = "LIMIT"; + + /** + * If true, the stream will be trimmed exactly. Equivalent to = in the + * Valkey API. Otherwise, the stream will be trimmed in a near-exact manner, which is more + * efficient, equivalent to ~ in the Valkey API. + */ + protected Boolean exact; + + /** If set, sets the maximal amount of entries that will be deleted. */ + protected Long limit; + + protected abstract String getMethod(); + + protected abstract String getThreshold(); + + protected List getValkeyApi() { + List optionArgs = new ArrayList<>(); + + optionArgs.add(this.getMethod()); + if (this.exact != null) { + optionArgs.add(this.exact ? TRIM_EXACT_VALKEY_API : TRIM_NOT_EXACT_VALKEY_API); + } + optionArgs.add(this.getThreshold()); + if (this.limit != null) { + optionArgs.add(TRIM_LIMIT_VALKEY_API); + optionArgs.add(this.limit.toString()); + } + + return optionArgs; + } + + /** + * Converts options for {@link StreamBaseCommands#xtrim(String, StreamTrimOptions)} into a + * String[]. + * + * @return String[] + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (this.getValkeyApi() != null) { + optionArgs.addAll(this.getValkeyApi()); + } + + return optionArgs.toArray(new String[0]); + } + + /** + * Converts options for {@link StreamBaseCommands#xtrim(String, StreamTrimOptions)} into a + * GlideString[]. + * + * @return GlideString[] + */ + public GlideString[] toGlideStringArgs() { + return Arrays.stream(toArgs()).map(GlideString::gs).toArray(GlideString[]::new); + } + + /** Option to trim the stream according to minimum ID. */ + public static class MinId extends StreamTrimOptions { + /** + * Trim the stream according to entry ID. Equivalent to MINID in the Valkey API. + */ + private final String threshold; + + /** + * Create a trim option to trim stream based on stream ID. + * + * @param threshold Comparison id. + */ + public MinId(@NonNull String threshold) { + this.threshold = threshold; + } + + /** + * Create a trim option to trim stream based on stream ID. + * + * @param threshold Comparison id. + */ + public MinId(@NonNull GlideString threshold) { + this.threshold = threshold.getString(); + } + + /** + * Create a trim option to trim stream based on stream ID. + * + * @param exact Whether to match exactly on the threshold. + * @param threshold Comparison id. + */ + public MinId(boolean exact, @NonNull String threshold) { + this.threshold = threshold; + this.exact = exact; + } + + /** + * Create a trim option to trim stream based on stream ID. + * + * @param exact Whether to match exactly on the threshold. + * @param threshold Comparison id. + */ + public MinId(boolean exact, @NonNull GlideString threshold) { + this.threshold = threshold.getString(); + this.exact = exact; + } + + /** + * Create a trim option to trim stream based on stream ID. + * + * @param threshold Comparison id. + * @param limit Max number of stream entries to be trimmed for non-exact match. + */ + public MinId(@NonNull String threshold, long limit) { + this.exact = false; + this.threshold = threshold; + this.limit = limit; + } + + /** + * Create a trim option to trim stream based on stream ID. + * + * @param threshold Comparison id. + * @param limit Max number of stream entries to be trimmed for non-exact match. + */ + public MinId(@NonNull GlideString threshold, long limit) { + this.exact = false; + this.threshold = threshold.getString(); + this.limit = limit; + } + + @Override + protected String getMethod() { + return TRIM_MINID_VALKEY_API; + } + + @Override + protected String getThreshold() { + return threshold; + } + } + + /** Option to trim the stream according to maximum stream length. */ + public static class MaxLen extends StreamTrimOptions { + /** + * Trim the stream according to length.
+ * Equivalent to MAXLEN in the Valkey API. + */ + private final Long threshold; + + /** + * Create a Max Length trim option to trim stream based on length. + * + * @param threshold Comparison count. + */ + public MaxLen(long threshold) { + this.threshold = threshold; + } + + /** + * Create a Max Length trim option to trim stream based on length. + * + * @param exact Whether to match exactly on the threshold. + * @param threshold Comparison count. + */ + public MaxLen(boolean exact, long threshold) { + this.threshold = threshold; + this.exact = exact; + } + + /** + * Create a Max Length trim option to trim stream entries exceeds the threshold. + * + * @param threshold Comparison count. + * @param limit Max number of stream entries to be trimmed for non-exact match. + */ + public MaxLen(long threshold, long limit) { + this.exact = false; + this.threshold = threshold; + this.limit = limit; + } + + @Override + protected String getMethod() { + return TRIM_MAXLEN_VALKEY_API; + } + + @Override + protected String getThreshold() { + return threshold.toString(); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/stream/XInfoStreamOptions.java b/java/client/src/main/java/glide/api/models/commands/stream/XInfoStreamOptions.java new file mode 100644 index 0000000000..7f26b1f857 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/stream/XInfoStreamOptions.java @@ -0,0 +1,17 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.stream; + +import glide.api.commands.StreamBaseCommands; +import glide.api.models.BaseTransaction; + +/** + * Valkey API Keywords for XINFO STREAM command represented by {@link + * StreamBaseCommands#xinfoStreamFull} and {@link BaseTransaction#xinfoStreamFull}. + */ +public abstract class XInfoStreamOptions { + /** Used by XINFO STREAM to query detailed info. */ + public static final String FULL = "FULL"; + + /** Used by XINFO STREAM to limit count of PEL entries. */ + public static final String COUNT = "COUNT"; +} diff --git a/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java b/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java index 38d18c0286..c2e8d0f0c1 100644 --- a/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java +++ b/java/client/src/main/java/glide/api/models/configuration/BackoffStrategy.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.configuration; import lombok.Builder; @@ -12,6 +12,15 @@ * *

Once the maximum value is reached, that will remain the time between retry attempts until a * reconnect attempt is successful. The client will attempt to reconnect indefinitely. + * + * @example + *

{@code
+ * BackoffStrategy reconnectionConfiguration = BackoffStrategy.builder()
+ *     .numOfRetries(5)
+ *     .exponentBase(2)
+ *     .factor(3)
+ *     .build()
+ * }
*/ @Getter @Builder diff --git a/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java index f22002f183..b6cc4e26ff 100644 --- a/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.configuration; import glide.connectors.resources.ThreadPoolResource; @@ -10,8 +10,8 @@ import lombok.experimental.SuperBuilder; /** - * Configuration settings class for creating a Redis Client. Shared settings for standalone and - * cluster clients. + * Configuration settings class for creating a client. Shared settings for standalone and cluster + * clients. */ @Getter @SuperBuilder @@ -43,7 +43,7 @@ public abstract class BaseClientConfiguration { * Credentials for authentication process. If none are set, the client will not authenticate * itself with the server. */ - private final RedisCredentials credentials; + private final ServerCredentials credentials; /** * The duration in milliseconds that the client should wait for a request to complete. This @@ -64,4 +64,6 @@ public abstract class BaseClientConfiguration { * loop group. If set, users are responsible for shutting the resource down when no longer in use. */ private final ThreadPoolResource threadPoolResource; + + public abstract BaseSubscriptionConfiguration getSubscriptionConfiguration(); } diff --git a/java/client/src/main/java/glide/api/models/configuration/BaseSubscriptionConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/BaseSubscriptionConfiguration.java new file mode 100644 index 0000000000..d97f8f64a8 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/BaseSubscriptionConfiguration.java @@ -0,0 +1,117 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import glide.api.BaseClient; +import glide.api.models.GlideString; +import glide.api.models.PubSubMessage; +import glide.api.models.configuration.ClusterSubscriptionConfiguration.ClusterSubscriptionConfigurationBuilder; +import glide.api.models.configuration.ClusterSubscriptionConfiguration.PubSubClusterChannelMode; +import glide.api.models.configuration.StandaloneSubscriptionConfiguration.PubSubChannelMode; +import glide.api.models.configuration.StandaloneSubscriptionConfiguration.StandaloneSubscriptionConfigurationBuilder; +import glide.api.models.exceptions.ConfigurationError; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Client subscription configuration. Could be either {@link StandaloneSubscriptionConfiguration} or + * {@link ClusterSubscriptionConfiguration}. + */ +@Getter +@RequiredArgsConstructor +public abstract class BaseSubscriptionConfiguration { + + /** + * A channel subscription mode. Could be either {@link PubSubChannelMode} or {@link + * PubSubClusterChannelMode}. + */ + public interface ChannelMode {} + + /** + * Callback called for every incoming message. It should be a fast, non-blocking operation to + * avoid issues. A next call could happen even before then the previous call complete.
+ * The callback arguments are: + * + *
    + *
  1. A received {@link PubSubMessage}. + *
  2. A user-defined {@link #context} or null if not configured. + *
+ */ + public interface MessageCallback extends BiConsumer {} + + /** + * Optional callback to accept the incoming messages. See {@link MessageCallback}.
+ * If not set, messages will be available via {@link BaseClient#tryGetPubSubMessage()} or {@link + * BaseClient#getPubSubMessage()}. + */ + protected final Optional callback; + + /** + * Optional arbitrary context, which will be passed to callback along with all received messages. + *
+ * Could be used to distinguish clients if multiple clients use a shared callback. + */ + protected final Optional context; + + // All code below is a custom implementation of `SuperBuilder`, because we provide + // custom user-friendly API `callback` and `subscription`. + /** + * Superclass for {@link ClusterSubscriptionConfigurationBuilder} and for {@link + * StandaloneSubscriptionConfigurationBuilder}. + */ + public abstract static class BaseSubscriptionConfigurationBuilder< + B extends BaseSubscriptionConfigurationBuilder, + C extends BaseSubscriptionConfiguration> { + + protected Optional callback = Optional.empty(); + protected Optional context = Optional.empty(); + + protected void addSubscription( + Map> subscriptions, M mode, GlideString channelOrPattern) { + if (!subscriptions.containsKey(mode)) { + // Note: Use a LinkedHashSet to preserve order for ease of debugging and unit testing. + subscriptions.put(mode, new LinkedHashSet<>()); + } + subscriptions.get(mode).add(channelOrPattern); + } + + protected abstract B self(); + + protected abstract C build(); + + /** + * Set a callback and a context. + * + * @param callback The {@link #callback}. This can be null to unset the callback. + * @param context The {@link #context}. + */ + public B callback(MessageCallback callback, Object context) { + if (context != null && callback == null) { + throw new ConfigurationError( + "PubSub subscriptions with a context require a callback function to be configured."); + } + this.callback = Optional.ofNullable(callback); + this.context = Optional.ofNullable(context); + return self(); + } + + /** + * Set a callback without context. null will be supplied to all callback calls as a + * context. + * + * @param callback The {@link #callback}. This can be null to unset the callback. + */ + public B callback(MessageCallback callback) { + if (callback == null && this.context.isPresent()) { + throw new ConfigurationError( + "PubSub subscriptions with a context require a callback function to be configured."); + } + this.callback = Optional.ofNullable(callback); + return self(); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java new file mode 100644 index 0000000000..a29ddd3d83 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/ClusterSubscriptionConfiguration.java @@ -0,0 +1,125 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import glide.api.GlideClusterClient; +import glide.api.models.GlideString; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.Getter; + +/** + * Subscription configuration for {@link GlideClusterClient}. + * + * @example + *
{@code
+ * // Configuration with 3 subscriptions and a callback:
+ * ClusterSubscriptionConfiguration subscriptionConfiguration =
+ *     ClusterSubscriptionConfiguration.builder()
+ *         .subscription(EXACT, "notifications")
+ *         .subscription(EXACT, "news")
+ *         .subscription(SHARDED, "data")
+ *         .callback(callback)
+ *         .build();
+ * // Now it could be supplied to `GlideClusterClientConfiguration`:
+ * GlideClusterClientConfiguration clientConfiguration =
+ *     GlideClusterClientConfiguration.builder()
+ *         .address(NodeAddress.builder().port(6379).build())
+ *         .subscriptionConfiguration(subscriptionConfiguration)
+ *         .build();
+ * }
+ */ +@Getter +public final class ClusterSubscriptionConfiguration extends BaseSubscriptionConfiguration { + + /** + * Describes subscription modes for cluster client. + * + * @see valkey.io for details. + */ + public enum PubSubClusterChannelMode implements ChannelMode { + /** Use exact channel names. */ + EXACT, + /** Use glob-style channel name patterns. */ + PATTERN, + /** + * Use sharded pubsub. + * + * @since Valkey 7.0 and above. + */ + SHARDED, + } + + /** + * PubSub subscriptions to be used for the client.
+ * Will be applied via SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE + * commands during connection establishment. + */ + private final Map> subscriptions; + + // All code below is a custom implementation of `SuperBuilder` + private ClusterSubscriptionConfiguration( + Optional callback, + Optional context, + Map> subscriptions) { + super(callback, context); + this.subscriptions = subscriptions; + } + + public static ClusterSubscriptionConfigurationBuilder builder() { + return new ClusterSubscriptionConfigurationBuilder(); + } + + /** Builder for {@link ClusterSubscriptionConfiguration}. */ + public static final class ClusterSubscriptionConfigurationBuilder + extends BaseSubscriptionConfigurationBuilder< + ClusterSubscriptionConfigurationBuilder, ClusterSubscriptionConfiguration> { + + private ClusterSubscriptionConfigurationBuilder() {} + + private Map> subscriptions = new HashMap<>(3); + + /** + * Add a subscription to a channel or to multiple channels if {@link + * PubSubClusterChannelMode#PATTERN} is used.
+ * See {@link ClusterSubscriptionConfiguration#subscriptions}. + */ + public ClusterSubscriptionConfigurationBuilder subscription( + PubSubClusterChannelMode mode, GlideString channelOrPattern) { + addSubscription(subscriptions, mode, channelOrPattern); + return this; + } + + /** + * Set all subscriptions in a bulk. Rewrites previously stored configurations.
+ * See {@link ClusterSubscriptionConfiguration#subscriptions}. + */ + public ClusterSubscriptionConfigurationBuilder subscriptions( + Map> subscriptions) { + this.subscriptions = subscriptions; + return this; + } + + /** + * Set subscriptions in a bulk for the given mode. Rewrites previously stored configurations for + * that mode.
+ * See {@link ClusterSubscriptionConfiguration#subscriptions}. + */ + public ClusterSubscriptionConfigurationBuilder subscriptions( + PubSubClusterChannelMode mode, Set subscriptions) { + this.subscriptions.put(mode, subscriptions); + return this; + } + + @Override + protected ClusterSubscriptionConfigurationBuilder self() { + return this; + } + + @Override + public ClusterSubscriptionConfiguration build() { + return new ClusterSubscriptionConfiguration(callback, context, subscriptions); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java new file mode 100644 index 0000000000..5c0f04c945 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java @@ -0,0 +1,39 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import glide.api.GlideClient; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +/** + * Represents the configuration settings for a Standalone {@link GlideClient}. + * + * @example + *
{@code
+ * GlideClientConfiguration glideClientConfiguration =
+ *     GlideClientConfiguration.builder()
+ *         .address(node1address)
+ *         .address(node2address)
+ *         .useTLS(true)
+ *         .readFrom(ReadFrom.PREFER_REPLICA)
+ *         .credentials(credentialsConfiguration)
+ *         .requestTimeout(2000)
+ *         .reconnectStrategy(reconnectionConfiguration)
+ *         .databaseId(1)
+ *         .clientName("GLIDE")
+ *         .subscriptionConfiguration(subscriptionConfiguration)
+ *         .build();
+ * }
+ */ +@Getter +@SuperBuilder +public class GlideClientConfiguration extends BaseClientConfiguration { + /** Strategy used to determine how and when to reconnect, in case of connection failures. */ + private final BackoffStrategy reconnectStrategy; + + /** Index of the logical database to connect to. */ + private final Integer databaseId; + + /** Subscription configuration for the current client. */ + private final StandaloneSubscriptionConfiguration subscriptionConfiguration; +} diff --git a/java/client/src/main/java/glide/api/models/configuration/GlideClusterClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/GlideClusterClientConfiguration.java new file mode 100644 index 0000000000..2e49e7b66d --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/GlideClusterClientConfiguration.java @@ -0,0 +1,34 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import glide.api.GlideClusterClient; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +/** + * Represents the configuration settings for a Cluster mode client {@link GlideClusterClient}. + * + * @apiNote Currently, the reconnection strategy in cluster mode is not configurable, and + * exponential backoff with fixed values is used. + * @example + *
{@code
+ * GlideClientConfiguration glideClientConfiguration =
+ *     GlideClientConfiguration.builder()
+ *         .address(node1address)
+ *         .address(node2address)
+ *         .useTLS(true)
+ *         .readFrom(ReadFrom.PREFER_REPLICA)
+ *         .credentials(credentialsConfiguration)
+ *         .requestTimeout(2000)
+ *         .clientName("GLIDE")
+ *         .subscriptionConfiguration(subscriptionConfiguration)
+ *         .build();
+ * }
+ */ +@SuperBuilder +@Getter +public class GlideClusterClientConfiguration extends BaseClientConfiguration { + + /** Subscription configuration for the current client. */ + private final ClusterSubscriptionConfiguration subscriptionConfiguration; +} diff --git a/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java b/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java index c52f70911d..b3766bad3b 100644 --- a/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java +++ b/java/client/src/main/java/glide/api/models/configuration/NodeAddress.java @@ -1,16 +1,25 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.configuration; import lombok.Builder; import lombok.Getter; import lombok.NonNull; -/** Represents the address and port of a node in the cluster. */ +/** + * Represents the address and port of a node in the cluster or in standalone installation. + * + * @example + *
{@code
+ * NodeAddress address1 = NodeAddress.builder().build(); // default parameters: localhost:6379
+ * NodeAddress address2 = NodeAddress.builder().port(6380).build(); // localhost:6380
+ * NodeAddress address2 = NodeAddress.builder().address("my.cloud.com").port(12345).build(); // custom address
+ * }
+ */ @Getter @Builder public class NodeAddress { - public static String DEFAULT_HOST = "localhost"; - public static Integer DEFAULT_PORT = 6379; + public static final String DEFAULT_HOST = "localhost"; + public static final Integer DEFAULT_PORT = 6379; @NonNull @Builder.Default private final String host = DEFAULT_HOST; @NonNull @Builder.Default private final Integer port = DEFAULT_PORT; diff --git a/java/client/src/main/java/glide/api/models/configuration/ReadFrom.java b/java/client/src/main/java/glide/api/models/configuration/ReadFrom.java index d7510718af..2d80ae7b60 100644 --- a/java/client/src/main/java/glide/api/models/configuration/ReadFrom.java +++ b/java/client/src/main/java/glide/api/models/configuration/ReadFrom.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.configuration; /** Represents the client's read from strategy. */ diff --git a/java/client/src/main/java/glide/api/models/configuration/RedisClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/RedisClientConfiguration.java deleted file mode 100644 index 6edab11c5d..0000000000 --- a/java/client/src/main/java/glide/api/models/configuration/RedisClientConfiguration.java +++ /dev/null @@ -1,16 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api.models.configuration; - -import lombok.Getter; -import lombok.experimental.SuperBuilder; - -/** Represents the configuration settings for a Standalone Redis client. */ -@Getter -@SuperBuilder -public class RedisClientConfiguration extends BaseClientConfiguration { - /** Strategy used to determine how and when to reconnect, in case of connection failures. */ - private final BackoffStrategy reconnectStrategy; - - /** Index of the logical database to connect to. */ - private final Integer databaseId; -} diff --git a/java/client/src/main/java/glide/api/models/configuration/RedisClusterClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/RedisClusterClientConfiguration.java deleted file mode 100644 index 0335bd75c2..0000000000 --- a/java/client/src/main/java/glide/api/models/configuration/RedisClusterClientConfiguration.java +++ /dev/null @@ -1,12 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api.models.configuration; - -import lombok.experimental.SuperBuilder; - -/** - * Represents the configuration settings for a Cluster Redis client. Notes: Currently, the - * reconnection strategy in cluster mode is not configurable, and exponential backoff with fixed - * values is used. - */ -@SuperBuilder -public class RedisClusterClientConfiguration extends BaseClientConfiguration {} diff --git a/java/client/src/main/java/glide/api/models/configuration/RedisCredentials.java b/java/client/src/main/java/glide/api/models/configuration/RedisCredentials.java deleted file mode 100644 index ac72031c4f..0000000000 --- a/java/client/src/main/java/glide/api/models/configuration/RedisCredentials.java +++ /dev/null @@ -1,20 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api.models.configuration; - -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; - -/** Represents the credentials for connecting to a Redis server. */ -@Getter -@Builder -public class RedisCredentials { - /** The password that will be used for authenticating connections to the Redis servers. */ - @NonNull private final String password; - - /** - * The username that will be used for authenticating connections to the Redis servers. If not - * supplied, "default" will be used. - */ - private final String username; -} diff --git a/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java index bea04a7e75..982f3d53cc 100644 --- a/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/RequestRoutingConfiguration.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.configuration; import glide.api.models.exceptions.RequestException; @@ -31,7 +31,12 @@ public interface MultiNodeRoute extends Route {} @RequiredArgsConstructor @Getter public enum SimpleSingleNodeRoute implements SingleNodeRoute { - /** Route request to a random node. */ + /** + * Route request to a random node.
+ * Warning
+ * Don't use it with write commands, because they could be randomly routed to a replica (RO) + * node and fail. + */ RANDOM(2); private final int ordinal; @@ -40,7 +45,10 @@ public enum SimpleSingleNodeRoute implements SingleNodeRoute { @RequiredArgsConstructor @Getter public enum SimpleMultiNodeRoute implements MultiNodeRoute { - /** Route request to all nodes. */ + /** + * Route request to all nodes. Warning
+ * Don't use it with write commands, they could be routed to a replica (RO) node and fail. + */ ALL_NODES(0), /** Route request to all primary nodes. */ ALL_PRIMARIES(1); @@ -65,7 +73,7 @@ public enum SlotType { @Getter public static class SlotIdRoute implements SingleNodeRoute { /** - * Slot number. There are 16384 slots in a redis cluster, and each shard manages a slot range. + * Slot number. There are 16384 slots in a Valkey cluster, and each shard manages a slot range. * Unless the slot is known, it's better to route using {@link SlotType#PRIMARY}. */ private final int slotId; diff --git a/java/client/src/main/java/glide/api/models/configuration/ServerCredentials.java b/java/client/src/main/java/glide/api/models/configuration/ServerCredentials.java new file mode 100644 index 0000000000..b4700e3441 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/ServerCredentials.java @@ -0,0 +1,35 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * Represents the credentials for connecting to a server. + * + * @example + *
{@code
+ * // credentials with username:
+ * ServerCredentials credentials1 = ServerCredentials.builder()
+ *     .username("GLIDE")
+ *     .build();
+ * // credentials with username and password:
+ * ServerCredentials credentials2 = ServerCredentials.builder()
+ *     .username("GLIDE")
+ *     .password(pwd)
+ *     .build();
+ * }
+ */ +@Getter +@Builder +public class ServerCredentials { + /** The password that will be used for authenticating connections to the servers. */ + @NonNull private final String password; + + /** + * The username that will be used for authenticating connections to the servers. If not supplied, + * "default" will be used. + */ + private final String username; +} diff --git a/java/client/src/main/java/glide/api/models/configuration/StandaloneSubscriptionConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/StandaloneSubscriptionConfiguration.java new file mode 100644 index 0000000000..bcc2229976 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/configuration/StandaloneSubscriptionConfiguration.java @@ -0,0 +1,119 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import glide.api.GlideClient; +import glide.api.models.GlideString; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.Getter; + +/** + * Subscription configuration for {@link GlideClient}. + * + * @example + *
{@code
+ * // Configuration with 2 subscriptions, a callback, and a context:
+ * StandaloneSubscriptionConfiguration subscriptionConfiguration =
+ *     StandaloneSubscriptionConfiguration.builder()
+ *         .subscription(EXACT, "notifications")
+ *         .subscription(PATTERN, "news.*")
+ *         .callback(callback, messageConsumer)
+ *         .build();
+ * // Now it could be supplied to `GlideClientConfiguration`:
+ * GlideClientConfiguration clientConfiguration =
+ *     GlideClientConfiguration.builder()
+ *         .address(NodeAddress.builder().port(6379).build())
+ *         .subscriptionConfiguration(subscriptionConfiguration)
+ *         .build();
+ * }
+ */ +@Getter +public final class StandaloneSubscriptionConfiguration extends BaseSubscriptionConfiguration { + + /** + * Describes subscription modes for standalone client. + * + * @see valkey.io for details. + */ + public enum PubSubChannelMode implements ChannelMode { + /** Use exact channel names. */ + EXACT, + /** Use glob-style channel name patterns. */ + PATTERN, + } + + /** + * PubSub subscriptions to be used for the client.
+ * Will be applied via SUBSCRIBE/PSUBSCRIBE commands during connection + * establishment. + */ + private final Map> subscriptions; + + // All code below is a custom implementation of `SuperBuilder` + public StandaloneSubscriptionConfiguration( + Optional callback, + Optional context, + Map> subscriptions) { + super(callback, context); + this.subscriptions = subscriptions; + } + + public static StandaloneSubscriptionConfigurationBuilder builder() { + return new StandaloneSubscriptionConfigurationBuilder(); + } + + /** Builder for {@link StandaloneSubscriptionConfiguration}. */ + public static final class StandaloneSubscriptionConfigurationBuilder + extends BaseSubscriptionConfigurationBuilder< + StandaloneSubscriptionConfigurationBuilder, StandaloneSubscriptionConfiguration> { + + private StandaloneSubscriptionConfigurationBuilder() {} + + // Note: Use a LinkedHashMap to preserve order for ease of debugging and unit testing. + private Map> subscriptions = new LinkedHashMap<>(2); + + /** + * Add a subscription to a channel or to multiple channels if {@link PubSubChannelMode#PATTERN} + * is used.
+ * See {@link StandaloneSubscriptionConfiguration#subscriptions}. + */ + public StandaloneSubscriptionConfigurationBuilder subscription( + PubSubChannelMode mode, GlideString channelOrPattern) { + addSubscription(subscriptions, mode, channelOrPattern); + return self(); + } + + /** + * Set all subscriptions in a bulk. Rewrites previously stored configurations.
+ * See {@link StandaloneSubscriptionConfiguration#subscriptions}. + */ + public StandaloneSubscriptionConfigurationBuilder subscriptions( + Map> subscriptions) { + this.subscriptions = subscriptions; + return this; + } + + /** + * Set subscriptions in a bulk for the given mode. Rewrites previously stored configurations for + * that mode.
+ * See {@link StandaloneSubscriptionConfiguration#subscriptions}. + */ + public StandaloneSubscriptionConfigurationBuilder subscriptions( + PubSubChannelMode mode, Set subscriptions) { + this.subscriptions.put(mode, subscriptions); + return this; + } + + @Override + protected StandaloneSubscriptionConfigurationBuilder self() { + return this; + } + + @Override + public StandaloneSubscriptionConfiguration build() { + return new StandaloneSubscriptionConfiguration(callback, context, subscriptions); + } + } +} diff --git a/java/client/src/main/java/glide/api/models/exceptions/ClosingException.java b/java/client/src/main/java/glide/api/models/exceptions/ClosingException.java index bf2ae85728..699dd5fe7a 100644 --- a/java/client/src/main/java/glide/api/models/exceptions/ClosingException.java +++ b/java/client/src/main/java/glide/api/models/exceptions/ClosingException.java @@ -1,8 +1,8 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.exceptions; -/** Redis client error: Errors that report that the client has closed and is no longer usable. */ -public class ClosingException extends RedisException { +/** Closed client error: Errors that report that the client has closed and is no longer usable. */ +public class ClosingException extends GlideException { public ClosingException(String message) { super(message); } diff --git a/java/client/src/main/java/glide/api/models/exceptions/ConfigurationError.java b/java/client/src/main/java/glide/api/models/exceptions/ConfigurationError.java new file mode 100644 index 0000000000..93131346e8 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/exceptions/ConfigurationError.java @@ -0,0 +1,9 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.exceptions; + +/** Errors that are thrown when a request cannot be completed in current configuration settings. */ +public class ConfigurationError extends GlideException { + public ConfigurationError(String message) { + super(message); + } +} diff --git a/java/client/src/main/java/glide/api/models/exceptions/ConnectionException.java b/java/client/src/main/java/glide/api/models/exceptions/ConnectionException.java index c6416464ee..8ee340ca2b 100644 --- a/java/client/src/main/java/glide/api/models/exceptions/ConnectionException.java +++ b/java/client/src/main/java/glide/api/models/exceptions/ConnectionException.java @@ -1,11 +1,11 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.exceptions; /** - * Redis client error: Errors that are thrown when a connection disconnects. These errors can be + * Connection error: Errors that are thrown when a connection disconnects. These errors can be * temporary, as the client will attempt to reconnect. */ -public class ConnectionException extends RedisException { +public class ConnectionException extends GlideException { public ConnectionException(String message) { super(message); } diff --git a/java/client/src/main/java/glide/api/models/exceptions/ExecAbortException.java b/java/client/src/main/java/glide/api/models/exceptions/ExecAbortException.java index 06c593d93c..34cfa93d7f 100644 --- a/java/client/src/main/java/glide/api/models/exceptions/ExecAbortException.java +++ b/java/client/src/main/java/glide/api/models/exceptions/ExecAbortException.java @@ -1,8 +1,8 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.exceptions; -/** Redis client error: Errors that are thrown when a transaction is aborted. */ -public class ExecAbortException extends RedisException { +/** Exec aborted error: Errors that are thrown when a transaction is aborted. */ +public class ExecAbortException extends GlideException { public ExecAbortException(String message) { super(message); } diff --git a/java/client/src/main/java/glide/api/models/exceptions/GlideException.java b/java/client/src/main/java/glide/api/models/exceptions/GlideException.java new file mode 100644 index 0000000000..10df49de71 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/exceptions/GlideException.java @@ -0,0 +1,9 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.exceptions; + +/** Glide client error: Base class for errors. */ +public class GlideException extends RuntimeException { + public GlideException(String message) { + super(message); + } +} diff --git a/java/client/src/main/java/glide/api/models/exceptions/RedisException.java b/java/client/src/main/java/glide/api/models/exceptions/RedisException.java deleted file mode 100644 index bb03b7c90b..0000000000 --- a/java/client/src/main/java/glide/api/models/exceptions/RedisException.java +++ /dev/null @@ -1,9 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api.models.exceptions; - -/** Redis client error: Base class for errors. */ -public class RedisException extends RuntimeException { - public RedisException(String message) { - super(message); - } -} diff --git a/java/client/src/main/java/glide/api/models/exceptions/RequestException.java b/java/client/src/main/java/glide/api/models/exceptions/RequestException.java index 420da9c4a2..dadf1788e6 100644 --- a/java/client/src/main/java/glide/api/models/exceptions/RequestException.java +++ b/java/client/src/main/java/glide/api/models/exceptions/RequestException.java @@ -1,8 +1,8 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.exceptions; -/** Redis client error: Errors that were reported during a request. */ -public class RequestException extends RedisException { +/** Request error: Errors that were reported during a request. */ +public class RequestException extends GlideException { public RequestException(String message) { super(message); } diff --git a/java/client/src/main/java/glide/api/models/exceptions/TimeoutException.java b/java/client/src/main/java/glide/api/models/exceptions/TimeoutException.java index e8be0cd4ae..63e370077b 100644 --- a/java/client/src/main/java/glide/api/models/exceptions/TimeoutException.java +++ b/java/client/src/main/java/glide/api/models/exceptions/TimeoutException.java @@ -1,8 +1,8 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models.exceptions; -/** Redis client error: Errors that are thrown when a request times out. */ -public class TimeoutException extends RedisException { +/** Timeout error: Errors that are thrown when a request times out. */ +public class TimeoutException extends GlideException { public TimeoutException(String message) { super(message); } diff --git a/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java b/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java index 6c5e86e2d2..2e11039420 100644 --- a/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java +++ b/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java @@ -1,6 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; +import static glide.api.logging.Logger.Level.ERROR; + +import glide.api.logging.Logger; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.ConnectionException; import glide.api.models.exceptions.ExecAbortException; @@ -21,6 +24,9 @@ @RequiredArgsConstructor public class CallbackDispatcher { + /** A message handler instance. */ + protected final MessageHandler messageHandler; + /** Unique request ID (callback ID). Thread-safe and overflow-safe. */ protected final AtomicInteger nextAvailableRequestId = new AtomicInteger(0); @@ -40,7 +46,7 @@ public class CallbackDispatcher { * search for a next free ID. */ // TODO: Optimize to avoid growing up to 2e32 (16 Gb) - // https://github.com/aws/glide-for-redis/issues/704 + // https://github.com/valkey-io/valkey-glide/issues/704 protected final ConcurrentLinkedQueue freeRequestIds = new ConcurrentLinkedQueue<>(); /** @@ -70,14 +76,19 @@ public CompletableFuture registerConnection() { * * @param response A response received */ - public void completeRequest(Response response) { + public void completeRequest(Response response) throws MessageHandler.MessageCallbackException { if (response.hasClosingError()) { - // According to https://github.com/aws/glide-for-redis/issues/851 + // According to https://github.com/valkey-io/valkey-glide/issues/851 // a response with a closing error may arrive with any/random callback ID (usually -1) // CommandManager and ConnectionManager would close the UDS channel on ClosingException distributeClosingException(response.getClosingError()); return; } + // pass pushes to the message handler and stop processing them + if (response.getIsPush()) { + messageHandler.handle(response); + return; + } // Complete and return the response at callbackId // free up the callback ID in the freeRequestIds list int callbackId = response.getCallbackIdx(); @@ -89,29 +100,37 @@ public void completeRequest(Response response) { String msg = error.getMessage(); switch (error.getType()) { case Unspecified: - // Unspecified error on Redis service-side + // Unspecified error on Valkey service-side future.completeExceptionally(new RequestException(msg)); + break; case ExecAbort: - // Transactional error on Redis service-side + // Transactional error on Valkey service-side future.completeExceptionally(new ExecAbortException(msg)); + break; case Timeout: - // Timeout from Glide to Redis service + // Timeout from Glide to Valkey service future.completeExceptionally(new TimeoutException(msg)); + break; case Disconnect: - // Connection problem between Glide and Redis + // Connection problem between Glide and Valkey future.completeExceptionally(new ConnectionException(msg)); + break; default: - // Request or command error from Redis + // Request or command error from Valkey future.completeExceptionally(new RequestException(msg)); } } future.completeAsync(() -> response); } else { - // TODO: log an error thru logger. // probably a response was received after shutdown or `registerRequest` call was missing - System.err.printf( - "Received a response for not registered callback id %d, request error = %s%n", - callbackId, response.getRequestError()); + Logger.log( + ERROR, + "callback dispatcher", + () -> + "Received a response for not registered callback id " + + callbackId + + ", request error = " + + response.getRequestError()); distributeClosingException("Client is in an erroneous state and should close"); } } diff --git a/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java b/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java index 4800316803..9a5836401c 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; +import command_request.CommandRequestOuterClass.CommandRequest; import connection_request.ConnectionRequestOuterClass.ConnectionRequest; import glide.connectors.resources.ThreadPoolResource; import io.netty.bootstrap.Bootstrap; @@ -12,7 +13,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import redis_request.RedisRequestOuterClass.RedisRequest; import response.ResponseOuterClass.Response; /** @@ -62,7 +62,7 @@ public ChannelHandler( * @param flush True to flush immediately * @return A response promise */ - public CompletableFuture write(RedisRequest.Builder request, boolean flush) { + public CompletableFuture write(CommandRequest.Builder request, boolean flush) { var commandId = callbackDispatcher.registerRequest(); request.setCallbackIdx(commandId.getKey()); diff --git a/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java b/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java new file mode 100644 index 0000000000..4a5b014f1a --- /dev/null +++ b/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java @@ -0,0 +1,216 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.connectors.handlers; + +import static glide.api.models.GlideString.gs; + +import glide.api.logging.Logger; +import glide.api.models.GlideString; +import glide.api.models.PubSubMessage; +import glide.api.models.configuration.BaseSubscriptionConfiguration.MessageCallback; +import glide.api.models.exceptions.GlideException; +import glide.managers.BaseResponseResolver; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import response.ResponseOuterClass.Response; + +/** Handler for incoming push messages (subscriptions). */ +@Getter +@RequiredArgsConstructor +public class MessageHandler { + + /** A wrapper for exceptions thrown from {@link MessageCallback} implementations. */ + static class MessageCallbackException extends Exception { + private MessageCallbackException(Exception cause) { + super(cause); + } + + @Override + public synchronized Exception getCause() { + // Overridden to restrict the return type to Exception rather than Throwable. + return (Exception) super.getCause(); + } + } + + // TODO maybe store `BaseSubscriptionConfiguration` as is? + /** + * A user callback to call for every incoming message, if given. If missing, messages are pushed + * into the {@link #queue}. + */ + private final Optional callback; + + /** An arbitrary user object to be passed to callback. */ + private final Optional context; + + /** Helper which extracts data from received {@link Response}s from GLIDE. */ + private final BaseResponseResolver responseResolver; + + /** A message queue wrapper. */ + @Getter private final PubSubMessageQueue queue = new PubSubMessageQueue(); + + /** Process a push (PUBSUB) message received as a part of {@link Response} from GLIDE. */ + void handle(Response response) throws MessageCallbackException { + Object data = responseResolver.apply(response); + if (!(data instanceof Map)) { + Logger.log( + Logger.Level.WARN, + "invalid push", + "Received invalid push: empty or in incorrect format."); + throw new GlideException("Received invalid push: empty or in incorrect format."); + } + @SuppressWarnings("unchecked") + Map push = (Map) data; + PushKind pushType = Enum.valueOf(PushKind.class, push.get("kind").toString()); + // The objects in values will actually be byte[]. + Object[] values = (Object[]) push.get("values"); + + switch (pushType) { + case Disconnection: + Logger.log( + Logger.Level.WARN, + "disconnect notification", + "Transport disconnected, messages might be lost"); + break; + case PMessage: + handle( + new PubSubMessage( + gs((byte[]) values[2]), gs((byte[]) values[1]), gs((byte[]) values[0]))); + return; + case Message: + case SMessage: + handle(new PubSubMessage(gs((byte[]) values[1]), gs((byte[]) values[0]))); + return; + case Subscribe: + case PSubscribe: + case SSubscribe: + case Unsubscribe: + case PUnsubscribe: + case SUnsubscribe: + // ignore for now + Logger.log( + Logger.Level.INFO, + "subscribe/unsubscribe notification", + () -> + String.format( + "Received push notification of type '%s': %s", + pushType, + Arrays.stream(values) + .map(v -> GlideString.of(v).toString()) + .collect(Collectors.joining(" ")))); + break; + default: + Logger.log( + Logger.Level.WARN, + "unknown notification", + () -> String.format("Unknown notification message: '%s'", pushType)); + } + } + + /** Process a {@link PubSubMessage} received. */ + private void handle(PubSubMessage message) throws MessageCallbackException { + if (callback.isPresent()) { + try { + callback.get().accept(message, context.orElse(null)); + } catch (Exception callbackException) { + throw new MessageCallbackException(callbackException); + } + // Note: Error subclasses are uncaught and will just propagate. + } else { + queue.push(message); + } + } + + /** Push type enum copy-pasted 1:1 from `redis-rs`. */ + enum PushKind { + /// `Disconnection` is sent from the **library** when connection is closed. + Disconnection, + /// Other kind to catch future kinds. + Other, + /// `invalidate` is received when a key is changed/deleted. + Invalidate, + /// `message` is received when pubsub message published by another client. + Message, + /// `pmessage` is received when pubsub message published by another client and client subscribed + // to topic via pattern. + PMessage, + /// `smessage` is received when pubsub message published by another client and client subscribed + // to it with sharding. + SMessage, + /// `unsubscribe` is received when client unsubscribed from a channel. + Unsubscribe, + /// `punsubscribe` is received when client unsubscribed from a pattern. + PUnsubscribe, + /// `sunsubscribe` is received when client unsubscribed from a shard channel. + SUnsubscribe, + /// `subscribe` is received when client subscribed to a channel. + Subscribe, + /// `psubscribe` is received when client subscribed to a pattern. + PSubscribe, + /// `ssubscribe` is received when client subscribed to a shard channel. + SSubscribe, + } + + /** + * An asynchronous FIFO message queue for {@link PubSubMessage} backed by {@link + * ConcurrentLinkedDeque}. + */ + public static class PubSubMessageQueue { + /** The queue itself. */ + final ConcurrentLinkedDeque messageQueue = new ConcurrentLinkedDeque<>(); + + /** + * A promise for the first incoming message. Returned to a user, if message queried in async + * manner, but nothing received yet. + */ + CompletableFuture firstMessagePromise = new CompletableFuture<>(); + + /** A flag whether a user already got a {@link #firstMessagePromise}. */ + private boolean firstMessagePromiseRequested = false; + + /** A private object used to synchronize {@link #push} and {@link #popAsync}. */ + private final Object lock = new Object(); + + // TODO Rework to remove or reduce `synchronized` blocks. + // If remove it now, some messages get reordered. + + /** Store a new message. */ + public void push(PubSubMessage message) { + synchronized (lock) { + if (firstMessagePromiseRequested) { + firstMessagePromiseRequested = false; + firstMessagePromise.complete(message); + firstMessagePromise = new CompletableFuture<>(); + return; + } + + messageQueue.addLast(message); + } + } + + /** Get a promise for a next message. */ + public CompletableFuture popAsync() { + synchronized (lock) { + PubSubMessage message = messageQueue.poll(); + if (message == null) { + // this makes first incoming message to be delivered into `firstMessagePromise` instead of + // `messageQueue` + firstMessagePromiseRequested = true; + return firstMessagePromise; + } + var future = new CompletableFuture(); + future.complete(message); + return future; + } + } + + /** Get a new message or null if nothing stored so far. */ + public PubSubMessage popSync() { + return messageQueue.poll(); + } + } +} diff --git a/java/client/src/main/java/glide/connectors/handlers/ProtobufSocketChannelInitializer.java b/java/client/src/main/java/glide/connectors/handlers/ProtobufSocketChannelInitializer.java index a52894cf2c..8d56a479e8 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ProtobufSocketChannelInitializer.java +++ b/java/client/src/main/java/glide/connectors/handlers/ProtobufSocketChannelInitializer.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; import io.netty.channel.ChannelInitializer; diff --git a/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java b/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java index 29b7f4c01b..6a9adbf93d 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java @@ -1,6 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; +import static glide.api.logging.Logger.Level.ERROR; + +import glide.api.logging.Logger; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import lombok.NonNull; @@ -16,7 +19,7 @@ public class ReadHandler extends ChannelInboundHandlerAdapter { /** Submit responses from glide to an instance {@link CallbackDispatcher} to handle them. */ @Override public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) - throws RuntimeException { + throws MessageHandler.MessageCallbackException { if (msg instanceof Response) { Response response = (Response) msg; callbackDispatcher.completeRequest(response); @@ -29,9 +32,20 @@ public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) /** Handles uncaught exceptions from {@link #channelRead(ChannelHandlerContext, Object)}. */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - // TODO: log thru logger - System.out.printf("=== exceptionCaught %s %s %n", ctx, cause); + if (cause instanceof MessageHandler.MessageCallbackException) { + Logger.log( + ERROR, + "read handler", + () -> "=== Exception thrown from pubsub callback " + ctx, + cause.getCause()); + + // Mimic the behavior of if this got thrown by a user thread. Print to stderr, + cause.printStackTrace(); + // Unwrap. Only works for Exceptions and not Errors. + throw ((MessageHandler.MessageCallbackException) cause).getCause(); + } + Logger.log(ERROR, "read handler", () -> "=== exceptionCaught " + ctx, cause); callbackDispatcher.distributeClosingException( "An unhandled error while reading from UDS channel: " + cause); } diff --git a/java/client/src/main/java/glide/connectors/resources/EpollResource.java b/java/client/src/main/java/glide/connectors/resources/EpollResource.java index ead5b53e50..642074d682 100644 --- a/java/client/src/main/java/glide/connectors/resources/EpollResource.java +++ b/java/client/src/main/java/glide/connectors/resources/EpollResource.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; import io.netty.channel.epoll.EpollDomainSocketChannel; diff --git a/java/client/src/main/java/glide/connectors/resources/KQueuePoolResource.java b/java/client/src/main/java/glide/connectors/resources/KQueuePoolResource.java index 53e9623515..5cacf80a01 100644 --- a/java/client/src/main/java/glide/connectors/resources/KQueuePoolResource.java +++ b/java/client/src/main/java/glide/connectors/resources/KQueuePoolResource.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; import io.netty.channel.kqueue.KQueueDomainSocketChannel; diff --git a/java/client/src/main/java/glide/connectors/resources/Platform.java b/java/client/src/main/java/glide/connectors/resources/Platform.java index 8846f0478e..9efc8cf8ad 100644 --- a/java/client/src/main/java/glide/connectors/resources/Platform.java +++ b/java/client/src/main/java/glide/connectors/resources/Platform.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; import io.netty.channel.epoll.Epoll; diff --git a/java/client/src/main/java/glide/connectors/resources/ThreadPoolResource.java b/java/client/src/main/java/glide/connectors/resources/ThreadPoolResource.java index b02380158b..772ff66e23 100644 --- a/java/client/src/main/java/glide/connectors/resources/ThreadPoolResource.java +++ b/java/client/src/main/java/glide/connectors/resources/ThreadPoolResource.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; import io.netty.channel.EventLoopGroup; diff --git a/java/client/src/main/java/glide/connectors/resources/ThreadPoolResourceAllocator.java b/java/client/src/main/java/glide/connectors/resources/ThreadPoolResourceAllocator.java index cf5629ccba..d221aa421f 100644 --- a/java/client/src/main/java/glide/connectors/resources/ThreadPoolResourceAllocator.java +++ b/java/client/src/main/java/glide/connectors/resources/ThreadPoolResourceAllocator.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; import java.util.function.Supplier; @@ -12,7 +12,7 @@ public class ThreadPoolResourceAllocator { * Sets up and returns the shared default ThreadPoolResource instance. On its first invocation, * this method creates and caches the defaultThreadPoolResource with a ThreadPoolResource instance * using the provided supplier. Subsequent calls return the cached instance, ensuring resource - * sharing among clients. If the current defaultThreadPoolResource’s ELG is in a shutting down + * sharing among clients. If the current defaultThreadPoolResource's ELG is in a shutting down * state, the cache is invalidated and a new default ThreadPoolResource is created and cached. * * @param supplier The supplier function used to create the ThreadPoolResource diff --git a/java/client/src/main/java/glide/ffi/resolvers/ClusterScanCursorResolver.java b/java/client/src/main/java/glide/ffi/resolvers/ClusterScanCursorResolver.java new file mode 100644 index 0000000000..8657bfa0f4 --- /dev/null +++ b/java/client/src/main/java/glide/ffi/resolvers/ClusterScanCursorResolver.java @@ -0,0 +1,22 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.ffi.resolvers; + +import glide.managers.CommandManager; + +/** + * Helper class for invoking JNI resources for {@link CommandManager.ClusterScanCursorDetail} + * implementations. + */ +public final class ClusterScanCursorResolver { + public static final String FINISHED_CURSOR_HANDLE; + + // TODO: consider lazy loading the glide_rs library + static { + NativeUtils.loadGlideLib(); + FINISHED_CURSOR_HANDLE = getFinishedCursorHandleConstant(); + } + + public static native void releaseNativeCursor(String cursor); + + public static native String getFinishedCursorHandleConstant(); +} diff --git a/java/client/src/main/java/glide/ffi/resolvers/GlideValueResolver.java b/java/client/src/main/java/glide/ffi/resolvers/GlideValueResolver.java new file mode 100644 index 0000000000..356b81f13b --- /dev/null +++ b/java/client/src/main/java/glide/ffi/resolvers/GlideValueResolver.java @@ -0,0 +1,52 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.ffi.resolvers; + +import response.ResponseOuterClass.Response; + +public class GlideValueResolver { + public static final long MAX_REQUEST_ARGS_LENGTH_IN_BYTES; + + // TODO: consider lazy loading the glide_rs library + static { + NativeUtils.loadGlideLib(); + + // Note: This is derived from a native call instead of hard-coded to ensure consistency + // between Java and native clients. + MAX_REQUEST_ARGS_LENGTH_IN_BYTES = getMaxRequestArgsLengthInBytes(); + } + + /** + * Resolve a value received from Valkey using given C-style pointer. String data is assumed to be + * UTF-8 and exposed as String objects. + * + * @param pointer A memory pointer from {@link Response} + * @return A RESP3 value + */ + public static native Object valueFromPointer(long pointer); + + /** + * Resolve a value received from Valkey using given C-style pointer. This method does not assume + * that strings are valid UTF-8 encoded strings and will expose this data as a byte[] + * . + * + * @param pointer A memory pointer from {@link Response} + * @return A RESP3 value + */ + public static native Object valueFromPointerBinary(long pointer); + + /** + * Copy the given array of byte arrays to a native series of byte arrays and return a C-style + * pointer. + * + * @param args The arguments to copy. + * @return A C-style pointer to a native representation of the arguments. + */ + public static native long createLeakedBytesVec(byte[][] args); + + /** + * Get the maximum length in bytes of all request arguments. + * + * @return The maximum length in bytes of all request arguments. + */ + private static native long getMaxRequestArgsLengthInBytes(); +} diff --git a/java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java b/java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java new file mode 100644 index 0000000000..4c1e2469cc --- /dev/null +++ b/java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java @@ -0,0 +1,13 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.ffi.resolvers; + +public class LoggerResolver { + // TODO: consider lazy loading the glide_rs library + static { + NativeUtils.loadGlideLib(); + } + + public static native int initInternal(int level, String fileName); + + public static native void logInternal(int level, String logIdentifier, String message); +} diff --git a/java/client/src/main/java/glide/ffi/resolvers/NativeUtils.java b/java/client/src/main/java/glide/ffi/resolvers/NativeUtils.java new file mode 100644 index 0000000000..c799c67672 --- /dev/null +++ b/java/client/src/main/java/glide/ffi/resolvers/NativeUtils.java @@ -0,0 +1,134 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.ffi.resolvers; + +import java.io.*; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.StandardCopyOption; + +/** + * A simple library class which helps with loading dynamic libraries stored in the JAR archive. + * These libraries usually contain implementation of some methods in native code (using JNI - Java + * Native Interface). + * + * @see https://raw.githubusercontent.com/adamheinrich/native-utils/master/src/main/java/cz/adamh/utils/NativeUtils.java + * @see https://github.com/adamheinrich/native-utils + */ +public class NativeUtils { + + /** + * The minimum length a prefix for a file has to have according to {@link + * File#createTempFile(String, String)}}. + */ + private static final int MIN_PREFIX_LENGTH = 3; + + public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils"; + + /** Temporary directory which will contain the dynamic library files. */ + private static File temporaryDir; + + /** Private constructor - this class will never be instanced */ + private NativeUtils() {} + + public static void loadGlideLib() { + String glideLib = "/libglide_rs"; + try { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("mac")) { + NativeUtils.loadLibraryFromJar(glideLib + ".dylib"); + } else if (osName.contains("linux")) { + NativeUtils.loadLibraryFromJar(glideLib + ".so"); + } else { + throw new UnsupportedOperationException( + "OS not supported. Glide is only available on Mac OS and Linux systems."); + } + } catch (java.io.IOException e) { + e.printStackTrace(); + } + } + + /** + * Loads library from current JAR archive + * + *

The file from JAR is copied into system temporary directory and then loaded. The temporary + * file is deleted after exiting. Method uses String as filename because the pathname is + * "abstract", not system-dependent. + * + * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. + * /package/File.ext + * @throws IOException If temporary file creation or read/write operation fails + * @throws IllegalArgumentException If source file (param path) does not exist + * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than + * MIN_PREFIX_LENGTH (restriction of {@link File#createTempFile(java.lang.String, + * java.lang.String)}). + * @throws FileNotFoundException If the file could not be found inside the JAR. + */ + public static void loadLibraryFromJar(String path) throws IOException { + + if (null == path || !path.startsWith("/")) { + throw new IllegalArgumentException("The path has to be absolute (start with '/')."); + } + + // Obtain filename from path + String[] parts = path.split("/"); + String filename = (parts.length > 1) ? parts[parts.length - 1] : null; + + // Check if the filename is okay + if (filename == null || filename.length() < MIN_PREFIX_LENGTH) { + throw new IllegalArgumentException( + "The filename has to be at least " + MIN_PREFIX_LENGTH + " characters long."); + } + + // Prepare temporary file + if (temporaryDir == null) { + temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX); + temporaryDir.deleteOnExit(); + } + + File temp = new File(temporaryDir, filename); + + try (InputStream is = NativeUtils.class.getResourceAsStream(path)) { + Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + temp.delete(); + throw e; + } catch (NullPointerException e) { + temp.delete(); + throw new FileNotFoundException("File " + path + " was not found inside JAR."); + } + + try { + System.load(temp.getAbsolutePath()); + } finally { + if (isPosixCompliant()) { + // Assume POSIX compliant file system, can be deleted after loading + temp.delete(); + } else { + // Assume non-POSIX, and don't delete until last file descriptor closed + temp.deleteOnExit(); + } + } + } + + private static boolean isPosixCompliant() { + try { + return FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + } catch (FileSystemNotFoundException | ProviderNotFoundException | SecurityException e) { + return false; + } + } + + private static File createTempDirectory(String prefix) throws IOException { + String tempDir = System.getProperty("java.io.tmpdir"); + File generatedDir = new File(tempDir, prefix + System.nanoTime()); + + if (!generatedDir.mkdir()) + throw new IOException("Failed to create temp directory " + generatedDir.getName()); + + return generatedDir; + } +} diff --git a/java/client/src/main/java/glide/ffi/resolvers/ObjectTypeResolver.java b/java/client/src/main/java/glide/ffi/resolvers/ObjectTypeResolver.java new file mode 100644 index 0000000000..deaaa54b68 --- /dev/null +++ b/java/client/src/main/java/glide/ffi/resolvers/ObjectTypeResolver.java @@ -0,0 +1,37 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.ffi.resolvers; + +import glide.api.models.commands.scan.ScanOptions; + +/** Helper class for invoking JNI resources for the {@link ScanOptions.ObjectType} enum. */ +public class ObjectTypeResolver { + public static final String OBJECT_TYPE_STRING_NATIVE_NAME; + public static final String OBJECT_TYPE_LIST_NATIVE_NAME; + public static final String OBJECT_TYPE_SET_NATIVE_NAME; + public static final String OBJECT_TYPE_ZSET_NATIVE_NAME; + public static final String OBJECT_TYPE_HASH_NATIVE_NAME; + public static final String OBJECT_TYPE_STREAM_NATIVE_NAME; + + // TODO: consider lazy loading the glide_rs library + static { + NativeUtils.loadGlideLib(); + OBJECT_TYPE_STRING_NATIVE_NAME = getTypeStringConstant(); + OBJECT_TYPE_LIST_NATIVE_NAME = getTypeListConstant(); + OBJECT_TYPE_SET_NATIVE_NAME = getTypeSetConstant(); + OBJECT_TYPE_ZSET_NATIVE_NAME = getTypeZSetConstant(); + OBJECT_TYPE_HASH_NATIVE_NAME = getTypeHashConstant(); + OBJECT_TYPE_STREAM_NATIVE_NAME = getTypeStreamConstant(); + } + + public static native String getTypeStringConstant(); + + public static native String getTypeListConstant(); + + public static native String getTypeSetConstant(); + + public static native String getTypeZSetConstant(); + + public static native String getTypeHashConstant(); + + public static native String getTypeStreamConstant(); +} diff --git a/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java b/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java deleted file mode 100644 index 054c2f0c3c..0000000000 --- a/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java +++ /dev/null @@ -1,20 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.ffi.resolvers; - -import response.ResponseOuterClass.Response; - -public class RedisValueResolver { - - // TODO: consider lazy loading the glide_rs library - static { - System.loadLibrary("glide_rs"); - } - - /** - * Resolve a value received from Redis using given C-style pointer. - * - * @param pointer A memory pointer from {@link Response} - * @return A RESP3 value - */ - public static native Object valueFromPointer(long pointer); -} diff --git a/java/client/src/main/java/glide/ffi/resolvers/ScriptResolver.java b/java/client/src/main/java/glide/ffi/resolvers/ScriptResolver.java index cf2b98daba..06f23a08e8 100644 --- a/java/client/src/main/java/glide/ffi/resolvers/ScriptResolver.java +++ b/java/client/src/main/java/glide/ffi/resolvers/ScriptResolver.java @@ -1,11 +1,11 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.ffi.resolvers; public class ScriptResolver { // TODO: consider lazy loading the glide_rs library static { - System.loadLibrary("glide_rs"); + NativeUtils.loadGlideLib(); } /** @@ -14,7 +14,7 @@ public class ScriptResolver { * @param code The Lua script * @return String representing the saved hash */ - public static native String storeScript(String code); + public static native String storeScript(byte[] code); /** * Unload or drop the stored Lua script from the script cache. diff --git a/java/client/src/main/java/glide/ffi/resolvers/SocketListenerResolver.java b/java/client/src/main/java/glide/ffi/resolvers/SocketListenerResolver.java index 5de10a280f..0cb3bf613a 100644 --- a/java/client/src/main/java/glide/ffi/resolvers/SocketListenerResolver.java +++ b/java/client/src/main/java/glide/ffi/resolvers/SocketListenerResolver.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.ffi.resolvers; public class SocketListenerResolver { @@ -8,7 +8,7 @@ public class SocketListenerResolver { // TODO: consider lazy loading the glide_rs library static { - System.loadLibrary("glide_rs"); + NativeUtils.loadGlideLib(); } /** diff --git a/java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java b/java/client/src/main/java/glide/managers/BaseResponseResolver.java similarity index 66% rename from java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java rename to java/client/src/main/java/glide/managers/BaseResponseResolver.java index f9b7ed87ab..c8e5c25a6f 100644 --- a/java/client/src/main/java/glide/managers/BaseCommandResponseResolver.java +++ b/java/client/src/main/java/glide/managers/BaseResponseResolver.java @@ -1,27 +1,27 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; import static glide.api.BaseClient.OK; -import glide.api.models.exceptions.RedisException; +import glide.api.models.exceptions.GlideException; import lombok.AllArgsConstructor; import response.ResponseOuterClass.Response; /** - * Response resolver responsible for evaluating the Redis response object with a success or failure. + * Response resolver responsible for evaluating the Valkey response object with a success or + * failure. */ @AllArgsConstructor -public class BaseCommandResponseResolver - implements RedisExceptionCheckedFunction { +public class BaseResponseResolver implements GlideExceptionCheckedFunction { - private RedisExceptionCheckedFunction respPointerResolver; + private GlideExceptionCheckedFunction respPointerResolver; /** * Extracts value from the RESP pointer. * * @return A generic Object with the Response or null if the response is empty */ - public Object apply(Response response) throws RedisException { + public Object apply(Response response) throws GlideException { // Note: errors are already handled before in CallbackDispatcher assert !response.hasClosingError() : "Unhandled response closing error"; assert !response.hasRequestError() : "Unhandled response request error"; diff --git a/java/client/src/main/java/glide/managers/CommandManager.java b/java/client/src/main/java/glide/managers/CommandManager.java index 1325e64863..c934883bd1 100644 --- a/java/client/src/main/java/glide/managers/CommandManager.java +++ b/java/client/src/main/java/glide/managers/CommandManager.java @@ -1,9 +1,23 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; +import com.google.protobuf.ByteString; +import command_request.CommandRequestOuterClass; +import command_request.CommandRequestOuterClass.Command; +import command_request.CommandRequestOuterClass.Command.ArgsArray; +import command_request.CommandRequestOuterClass.CommandRequest; +import command_request.CommandRequestOuterClass.RequestType; +import command_request.CommandRequestOuterClass.Routes; +import command_request.CommandRequestOuterClass.ScriptInvocation; +import command_request.CommandRequestOuterClass.ScriptInvocationPointers; +import command_request.CommandRequestOuterClass.SimpleRoutes; +import command_request.CommandRequestOuterClass.SlotTypes; import glide.api.models.ClusterTransaction; +import glide.api.models.GlideString; import glide.api.models.Script; import glide.api.models.Transaction; +import glide.api.models.commands.scan.ClusterScanCursor; +import glide.api.models.commands.scan.ScanOptions; import glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute; @@ -14,19 +28,14 @@ import glide.api.models.exceptions.RequestException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; +import glide.ffi.resolvers.GlideValueResolver; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import lombok.NonNull; import lombok.RequiredArgsConstructor; -import redis_request.RedisRequestOuterClass; -import redis_request.RedisRequestOuterClass.Command; -import redis_request.RedisRequestOuterClass.Command.ArgsArray; -import redis_request.RedisRequestOuterClass.RedisRequest; -import redis_request.RedisRequestOuterClass.RequestType; -import redis_request.RedisRequestOuterClass.Routes; -import redis_request.RedisRequestOuterClass.ScriptInvocation; -import redis_request.RedisRequestOuterClass.SimpleRoutes; -import redis_request.RedisRequestOuterClass.SlotTypes; import response.ResponseOuterClass.Response; /** @@ -39,28 +48,58 @@ public class CommandManager { /** UDS connection representation. */ private final ChannelHandler channel; + /** + * Internal interface for exposing implementation details about a ClusterScanCursor. This is an + * interface so that it can be mocked in tests. + */ + public interface ClusterScanCursorDetail extends ClusterScanCursor { + /** + * Returns the handle String representing the cursor. + * + * @return the handle String representing the cursor. + */ + String getCursorHandle(); + } + /** * Build a command and send. * - * @param requestType Redis command type - * @param arguments Redis command arguments + * @param requestType Valkey command type + * @param arguments Valkey command arguments * @param responseHandler The handler for the response object * @return A result promise of type T */ public CompletableFuture submitNewCommand( RequestType requestType, String[] arguments, - RedisExceptionCheckedFunction responseHandler) { + GlideExceptionCheckedFunction responseHandler) { - RedisRequest.Builder command = prepareRedisRequest(requestType, arguments); + CommandRequest.Builder command = prepareCommandRequest(requestType, arguments); return submitCommandToChannel(command, responseHandler); } /** * Build a command and send. * - * @param requestType Redis command type - * @param arguments Redis command arguments + * @param requestType Valkey command type + * @param arguments Valkey command arguments + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewCommand( + RequestType requestType, + GlideString[] arguments, + GlideExceptionCheckedFunction responseHandler) { + + CommandRequest.Builder command = prepareCommandRequest(requestType, arguments); + return submitCommandToChannel(command, responseHandler); + } + + /** + * Build a command and send. + * + * @param requestType Valkey command type + * @param arguments Valkey command arguments * @param route Command routing parameters * @param responseHandler The handler for the response object * @return A result promise of type T @@ -69,28 +108,47 @@ public CompletableFuture submitNewCommand( RequestType requestType, String[] arguments, Route route, - RedisExceptionCheckedFunction responseHandler) { + GlideExceptionCheckedFunction responseHandler) { - RedisRequest.Builder command = prepareRedisRequest(requestType, arguments, route); + CommandRequest.Builder command = prepareCommandRequest(requestType, arguments, route); return submitCommandToChannel(command, responseHandler); } /** - * Build a Transaction and send. + * Build a command and send. * - * @param transaction Redis Transaction request with multiple commands + * @param requestType Valkey command type + * @param arguments Valkey command arguments + * @param route Command routing parameters * @param responseHandler The handler for the response object * @return A result promise of type T */ public CompletableFuture submitNewCommand( - Transaction transaction, RedisExceptionCheckedFunction responseHandler) { + RequestType requestType, + GlideString[] arguments, + Route route, + GlideExceptionCheckedFunction responseHandler) { - RedisRequest.Builder command = prepareRedisRequest(transaction); + CommandRequest.Builder command = prepareCommandRequest(requestType, arguments, route); return submitCommandToChannel(command, responseHandler); } /** - * Build a Script (by hash) request to send to Redis. + * Build a Transaction and send. + * + * @param transaction Transaction request with multiple commands + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewTransaction( + Transaction transaction, GlideExceptionCheckedFunction responseHandler) { + + CommandRequest.Builder command = prepareCommandRequest(transaction); + return submitCommandToChannel(command, responseHandler); + } + + /** + * Build a Script (by hash) request to send to Valkey. * * @param script Lua script hash object * @param keys The keys that are used in the script @@ -100,40 +158,57 @@ public CompletableFuture submitNewCommand( */ public CompletableFuture submitScript( Script script, - List keys, - List args, - RedisExceptionCheckedFunction responseHandler) { + List keys, + List args, + GlideExceptionCheckedFunction responseHandler) { - RedisRequest.Builder command = prepareRedisRequest(script, keys, args); + CommandRequest.Builder command = prepareScript(script, keys, args); return submitCommandToChannel(command, responseHandler); } /** - * Build a Transaction and send. + * Build a Cluster Transaction and send. * - * @param transaction Redis Transaction request with multiple commands + * @param transaction Transaction request with multiple commands * @param route Transaction routing parameters * @param responseHandler The handler for the response object * @return A result promise of type T */ - public CompletableFuture submitNewCommand( + public CompletableFuture submitNewTransaction( ClusterTransaction transaction, Optional route, - RedisExceptionCheckedFunction responseHandler) { + GlideExceptionCheckedFunction responseHandler) { + + CommandRequest.Builder command = prepareCommandRequest(transaction, route); + return submitCommandToChannel(command, responseHandler); + } + + /** + * Submits a scan request with cursor + * + * @param cursor Iteration cursor + * @param options {@link ScanOptions} + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitClusterScan( + ClusterScanCursor cursor, + @NonNull ScanOptions options, + GlideExceptionCheckedFunction responseHandler) { - RedisRequest.Builder command = prepareRedisRequest(transaction, route); + final CommandRequest.Builder command = prepareCursorRequest(cursor, options); return submitCommandToChannel(command, responseHandler); } /** - * Take a redis request and send to channel. + * Take a command request and send to channel. * - * @param command The Redis command request as a builder to execute + * @param command The command request as a builder to execute * @param responseHandler The handler for the response object * @return A result promise of type T */ protected CompletableFuture submitCommandToChannel( - RedisRequest.Builder command, RedisExceptionCheckedFunction responseHandler) { + CommandRequest.Builder command, GlideExceptionCheckedFunction responseHandler) { if (channel.isClosed()) { var errorFuture = new CompletableFuture(); errorFuture.completeExceptionally( @@ -152,101 +227,192 @@ protected CompletableFuture submitCommandToChannel( /** * Build a protobuf command request object with routing options. * - * @param requestType Redis command type - * @param arguments Redis command arguments + * @param requestType Valkey command type + * @param arguments Valkey command arguments * @param route Command routing parameters * @return An incomplete request. {@link CallbackDispatcher} is responsible to complete it by * adding a callback id. */ - protected RedisRequest.Builder prepareRedisRequest( + protected CommandRequest.Builder prepareCommandRequest( RequestType requestType, String[] arguments, Route route) { - ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); - for (var arg : arguments) { - commandArgs.addArgs(arg); - } + final Command.Builder commandBuilder = Command.newBuilder(); + populateCommandWithArgs(arguments, commandBuilder); var builder = - RedisRequest.newBuilder() - .setSingleCommand( - Command.newBuilder() - .setRequestType(requestType) - .setArgsArray(commandArgs.build()) - .build()); - - return prepareRedisRequestRoute(builder, route); + CommandRequest.newBuilder() + .setSingleCommand(commandBuilder.setRequestType(requestType).build()); + + return prepareCommandRequestRoute(builder, route); + } + + /** + * Build a protobuf command request object with routing options. + * + * @param requestType Valkey command type + * @param arguments Valkey command arguments + * @param route Command routing parameters + * @return An incomplete request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected CommandRequest.Builder prepareCommandRequest( + RequestType requestType, GlideString[] arguments, Route route) { + final Command.Builder commandBuilder = Command.newBuilder(); + populateCommandWithArgs(arguments, commandBuilder); + + var builder = + CommandRequest.newBuilder() + .setSingleCommand(commandBuilder.setRequestType(requestType).build()); + + return prepareCommandRequestRoute(builder, route); } /** * Build a protobuf transaction request object with routing options. * - * @param transaction Redis transaction with commands + * @param transaction Valkey transaction with commands * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by * adding a callback id. */ - protected RedisRequest.Builder prepareRedisRequest(Transaction transaction) { - return RedisRequest.newBuilder().setTransaction(transaction.getProtobufTransaction().build()); + protected CommandRequest.Builder prepareCommandRequest(Transaction transaction) { + return CommandRequest.newBuilder().setTransaction(transaction.getProtobufTransaction().build()); } /** * Build a protobuf Script Invoke request. * - * @param script Redis Script + * @param script Valkey Script * @param keys keys for the Script * @param args args for the Script * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by * adding a callback id. */ - protected RedisRequest.Builder prepareRedisRequest( - Script script, List keys, List args) { - return RedisRequest.newBuilder() + protected CommandRequest.Builder prepareScript( + Script script, List keys, List args) { + + if (keys.stream().mapToLong(key -> key.getBytes().length).sum() + + args.stream().mapToLong(key -> key.getBytes().length).sum() + > GlideValueResolver.MAX_REQUEST_ARGS_LENGTH_IN_BYTES) { + return CommandRequest.newBuilder() + .setScriptInvocationPointers( + ScriptInvocationPointers.newBuilder() + .setHash(script.getHash()) + .setArgsPointer( + GlideValueResolver.createLeakedBytesVec( + args.stream().map(GlideString::getBytes).toArray(byte[][]::new))) + .setKeysPointer( + GlideValueResolver.createLeakedBytesVec( + keys.stream().map(GlideString::getBytes).toArray(byte[][]::new))) + .build()); + } + + return CommandRequest.newBuilder() .setScriptInvocation( ScriptInvocation.newBuilder() .setHash(script.getHash()) - .addAllKeys(keys) - .addAllArgs(args) + .addAllKeys( + keys.stream() + .map(GlideString::getBytes) + .map(ByteString::copyFrom) + .collect(Collectors.toList())) + .addAllArgs( + args.stream() + .map(GlideString::getBytes) + .map(ByteString::copyFrom) + .collect(Collectors.toList())) .build()); } /** * Build a protobuf transaction request object with routing options. * - * @param transaction Redis transaction with commands + * @param transaction Valkey transaction with commands * @param route Command routing parameters * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by * adding a callback id. */ - protected RedisRequest.Builder prepareRedisRequest( + protected CommandRequest.Builder prepareCommandRequest( ClusterTransaction transaction, Optional route) { - RedisRequest.Builder builder = - RedisRequest.newBuilder().setTransaction(transaction.getProtobufTransaction().build()); + CommandRequest.Builder builder = + CommandRequest.newBuilder().setTransaction(transaction.getProtobufTransaction().build()); - return route.isPresent() ? prepareRedisRequestRoute(builder, route.get()) : builder; + return route.isPresent() ? prepareCommandRequestRoute(builder, route.get()) : builder; } /** - * Build a protobuf command request object. + * Build a protobuf cursor scan request. * - * @param requestType Redis command type - * @param arguments Redis command arguments + * @param cursor Iteration cursor + * @param options {@link ScanOptions} * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by * adding a callback id. */ - protected RedisRequest.Builder prepareRedisRequest(RequestType requestType, String[] arguments) { - ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); - for (var arg : arguments) { - commandArgs.addArgs(arg); + protected CommandRequest.Builder prepareCursorRequest( + @NonNull ClusterScanCursor cursor, @NonNull ScanOptions options) { + + CommandRequestOuterClass.ClusterScan.Builder clusterScanBuilder = + CommandRequestOuterClass.ClusterScan.newBuilder(); + + if (cursor != ClusterScanCursor.INITIAL_CURSOR_INSTANCE) { + if (cursor instanceof ClusterScanCursorDetail) { + clusterScanBuilder.setCursor(((ClusterScanCursorDetail) cursor).getCursorHandle()); + } else { + throw new IllegalArgumentException("Illegal cursor submitted."); + } } - return RedisRequest.newBuilder() - .setSingleCommand( - Command.newBuilder() - .setRequestType(requestType) - .setArgsArray(commandArgs.build()) - .build()); + // Use the binary match pattern first + if (options.getMatchPattern() != null) { + clusterScanBuilder.setMatchPattern(ByteString.copyFrom(options.getMatchPattern().getBytes())); + } + + if (options.getCount() != null) { + clusterScanBuilder.setCount(options.getCount()); + } + + if (options.getType() != null) { + clusterScanBuilder.setObjectType(options.getType().getNativeName()); + } + + return CommandRequest.newBuilder().setClusterScan(clusterScanBuilder.build()); + } + + /** + * Build a protobuf command request object. + * + * @param requestType Valkey command type + * @param arguments Valkey command arguments + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected CommandRequest.Builder prepareCommandRequest( + RequestType requestType, String[] arguments) { + final Command.Builder commandBuilder = Command.newBuilder(); + populateCommandWithArgs(arguments, commandBuilder); + + return CommandRequest.newBuilder() + .setSingleCommand(commandBuilder.setRequestType(requestType).build()); + } + + /** + * Build a protobuf command request object. + * + * @param requestType Valkey command type + * @param arguments Valkey command arguments + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected CommandRequest.Builder prepareCommandRequest( + RequestType requestType, GlideString[] arguments) { + final Command.Builder commandBuilder = Command.newBuilder(); + populateCommandWithArgs(arguments, commandBuilder); + + return CommandRequest.newBuilder() + .setSingleCommand(commandBuilder.setRequestType(requestType).build()); } - private RedisRequest.Builder prepareRedisRequestRoute(RedisRequest.Builder builder, Route route) { + private CommandRequest.Builder prepareCommandRequestRoute( + CommandRequest.Builder builder, Route route) { if (route instanceof SimpleMultiNodeRoute) { builder.setRoute( @@ -262,7 +428,7 @@ private RedisRequest.Builder prepareRedisRequestRoute(RedisRequest.Builder build builder.setRoute( Routes.newBuilder() .setSlotIdRoute( - RedisRequestOuterClass.SlotIdRoute.newBuilder() + CommandRequestOuterClass.SlotIdRoute.newBuilder() .setSlotId(((SlotIdRoute) route).getSlotId()) .setSlotType( SlotTypes.forNumber(((SlotIdRoute) route).getSlotType().ordinal())))); @@ -270,7 +436,7 @@ private RedisRequest.Builder prepareRedisRequestRoute(RedisRequest.Builder build builder.setRoute( Routes.newBuilder() .setSlotKeyRoute( - RedisRequestOuterClass.SlotKeyRoute.newBuilder() + CommandRequestOuterClass.SlotKeyRoute.newBuilder() .setSlotKey(((SlotKeyRoute) route).getSlotKey()) .setSlotType( SlotTypes.forNumber(((SlotKeyRoute) route).getSlotType().ordinal())))); @@ -278,7 +444,7 @@ private RedisRequest.Builder prepareRedisRequestRoute(RedisRequest.Builder build builder.setRoute( Routes.newBuilder() .setByAddressRoute( - RedisRequestOuterClass.ByAddressRoute.newBuilder() + CommandRequestOuterClass.ByAddressRoute.newBuilder() .setHost(((ByAddressRoute) route).getHost()) .setPort(((ByAddressRoute) route).getPort()))); } else { @@ -299,9 +465,62 @@ private Response exceptionHandler(Throwable e) { channel.close(); } if (e instanceof RuntimeException) { - // RedisException also goes here + // GlideException also goes here throw (RuntimeException) e; } throw new RuntimeException(e); } + + /** + * Add the given set of arguments to the output Command.Builder. + * + * @param arguments The arguments to add to the builder. + * @param outputBuilder The builder to populate with arguments. + */ + public static void populateCommandWithArgs( + ArgType[] arguments, Command.Builder outputBuilder) { + populateCommandWithArgs( + Arrays.stream(arguments) + .map(value -> GlideString.of(value).getBytes()) + .collect(Collectors.toList()), + outputBuilder); + } + + /** + * Add the given set of arguments to the output Command.Builder. + * + * @param arguments The arguments to add to the builder. + * @param outputBuilder The builder to populate with arguments. + */ + private static void populateCommandWithArgs( + GlideString[] arguments, Command.Builder outputBuilder) { + populateCommandWithArgs( + Arrays.stream(arguments).map(GlideString::getBytes).collect(Collectors.toList()), + outputBuilder); + } + + /** + * Add the given set of arguments to the output Command.Builder. + * + *

Implementation note: When the length in bytes of all arguments supplied to the given command + * exceed {@link GlideValueResolver#MAX_REQUEST_ARGS_LENGTH_IN_BYTES}, the Command will hold a + * handle to leaked vector of byte arrays in the native layer in the ArgsVecPointer + * field. In the normal case where the command arguments are small, they'll be serialized as to an + * {@link ArgsArray} message. + * + * @param arguments The arguments to add to the builder. + * @param outputBuilder The builder to populate with arguments. + */ + private static void populateCommandWithArgs( + List arguments, Command.Builder outputBuilder) { + final long totalArgSize = arguments.stream().mapToLong(arg -> arg.length).sum(); + if (totalArgSize < GlideValueResolver.MAX_REQUEST_ARGS_LENGTH_IN_BYTES) { + ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); + arguments.forEach(arg -> commandArgs.addArgs(ByteString.copyFrom(arg))); + outputBuilder.setArgsArray(commandArgs); + } else { + outputBuilder.setArgsVecPointer( + GlideValueResolver.createLeakedBytesVec(arguments.toArray(new byte[][] {}))); + } + } } diff --git a/java/client/src/main/java/glide/managers/ConnectionManager.java b/java/client/src/main/java/glide/managers/ConnectionManager.java index d9a8f58574..58328d375c 100644 --- a/java/client/src/main/java/glide/managers/ConnectionManager.java +++ b/java/client/src/main/java/glide/managers/ConnectionManager.java @@ -1,15 +1,18 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; +import com.google.protobuf.ByteString; import connection_request.ConnectionRequestOuterClass; import connection_request.ConnectionRequestOuterClass.AuthenticationInfo; import connection_request.ConnectionRequestOuterClass.ConnectionRequest; +import connection_request.ConnectionRequestOuterClass.PubSubChannelsOrPatterns; +import connection_request.ConnectionRequestOuterClass.PubSubSubscriptions; import connection_request.ConnectionRequestOuterClass.TlsMode; import glide.api.models.configuration.BaseClientConfiguration; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.ReadFrom; -import glide.api.models.configuration.RedisClientConfiguration; -import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.ChannelHandler; import java.util.concurrent.CompletableFuture; @@ -25,17 +28,17 @@ public class ConnectionManager { // TODO: consider making connection manager static, and moving the ChannelHandler to the - // RedisClient. + // GlideClient. /** UDS connection representation. */ private final ChannelHandler channel; /** - * Make a connection request to Redis Rust-core client. + * Make a connection request to Valkey Rust-core client. * * @param configuration Connection Request Configuration */ - public CompletableFuture connectToRedis(BaseClientConfiguration configuration) { + public CompletableFuture connectToValkey(BaseClientConfiguration configuration) { ConnectionRequest request = createConnectionRequest(configuration); return channel .connect(request) @@ -52,7 +55,7 @@ public CompletableFuture connectToRedis(BaseClientConfiguration configurat private Response exceptionHandler(Throwable e) { channel.close(); if (e instanceof RuntimeException) { - // RedisException also goes here + // GlideException also goes here throw (RuntimeException) e; } throw new RuntimeException(e); @@ -66,13 +69,13 @@ private Response exceptionHandler(Throwable e) { * @return ConnectionRequest protobuf message */ private ConnectionRequest createConnectionRequest(BaseClientConfiguration configuration) { - if (configuration instanceof RedisClusterClientConfiguration) { - return setupConnectionRequestBuilderRedisClusterClient( - (RedisClusterClientConfiguration) configuration) + if (configuration instanceof GlideClusterClientConfiguration) { + return setupConnectionRequestBuilderGlideClusterClient( + (GlideClusterClientConfiguration) configuration) .build(); } - return setupConnectionRequestBuilderRedisClient((RedisClientConfiguration) configuration) + return setupConnectionRequestBuilderGlideClient((GlideClientConfiguration) configuration) .build(); } @@ -119,12 +122,12 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderBaseConfiguration } /** - * Creates ConnectionRequestBuilder, so it has appropriate fields for the Redis Standalone Client. + * Creates ConnectionRequestBuilder, so it has appropriate fields for the Standalone Client. * * @param configuration Connection Request Configuration */ - private ConnectionRequest.Builder setupConnectionRequestBuilderRedisClient( - RedisClientConfiguration configuration) { + private ConnectionRequest.Builder setupConnectionRequestBuilderGlideClient( + GlideClientConfiguration configuration) { ConnectionRequest.Builder connectionRequestBuilder = setupConnectionRequestBuilderBaseConfiguration(configuration); connectionRequestBuilder.setClusterModeEnabled(false); @@ -141,21 +144,48 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderRedisClient( connectionRequestBuilder.setDatabaseId(configuration.getDatabaseId()); } + if (configuration.getSubscriptionConfiguration() != null) { + // TODO throw ConfigurationError if RESP2 + var subscriptionsBuilder = PubSubSubscriptions.newBuilder(); + for (var entry : configuration.getSubscriptionConfiguration().getSubscriptions().entrySet()) { + var channelsBuilder = PubSubChannelsOrPatterns.newBuilder(); + for (var channel : entry.getValue()) { + channelsBuilder.addChannelsOrPatterns(ByteString.copyFrom(channel.getBytes())); + } + subscriptionsBuilder.putChannelsOrPatternsByType( + entry.getKey().ordinal(), channelsBuilder.build()); + } + connectionRequestBuilder.setPubsubSubscriptions(subscriptionsBuilder.build()); + } + return connectionRequestBuilder; } /** - * Creates ConnectionRequestBuilder, so it has appropriate fields for the Redis Cluster Client. + * Creates ConnectionRequestBuilder, so it has appropriate fields for the Cluster Client. * * @param configuration */ - private ConnectionRequestOuterClass.ConnectionRequest.Builder - setupConnectionRequestBuilderRedisClusterClient( - RedisClusterClientConfiguration configuration) { + private ConnectionRequest.Builder setupConnectionRequestBuilderGlideClusterClient( + GlideClusterClientConfiguration configuration) { ConnectionRequest.Builder connectionRequestBuilder = setupConnectionRequestBuilderBaseConfiguration(configuration); connectionRequestBuilder.setClusterModeEnabled(true); + if (configuration.getSubscriptionConfiguration() != null) { + // TODO throw ConfigurationError if RESP2 + var subscriptionsBuilder = PubSubSubscriptions.newBuilder(); + for (var entry : configuration.getSubscriptionConfiguration().getSubscriptions().entrySet()) { + var channelsBuilder = PubSubChannelsOrPatterns.newBuilder(); + for (var channel : entry.getValue()) { + channelsBuilder.addChannelsOrPatterns(ByteString.copyFrom(channel.getBytes())); + } + subscriptionsBuilder.putChannelsOrPatternsByType( + entry.getKey().ordinal(), channelsBuilder.build()); + } + connectionRequestBuilder.setPubsubSubscriptions(subscriptionsBuilder.build()); + } + return connectionRequestBuilder; } diff --git a/java/client/src/main/java/glide/managers/GlideExceptionCheckedFunction.java b/java/client/src/main/java/glide/managers/GlideExceptionCheckedFunction.java new file mode 100644 index 0000000000..0a9b2eb3dc --- /dev/null +++ b/java/client/src/main/java/glide/managers/GlideExceptionCheckedFunction.java @@ -0,0 +1,24 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.managers; + +import glide.api.models.exceptions.GlideException; + +/** + * Functional Interface to convert values and throw GlideException when encountering an error state. + * + * @param type to evaluate + * @param payload type + */ +@FunctionalInterface +public interface GlideExceptionCheckedFunction { + + /** + * Functional response handler that takes a value of type R and returns a payload of type T. + * Throws GlideException when encountering an invalid or error state. + * + * @param value - received value type + * @return T - returning payload type + * @throws GlideException + */ + T apply(R value) throws GlideException; +} diff --git a/java/client/src/main/java/glide/managers/RedisExceptionCheckedFunction.java b/java/client/src/main/java/glide/managers/RedisExceptionCheckedFunction.java deleted file mode 100644 index 03312ec9a5..0000000000 --- a/java/client/src/main/java/glide/managers/RedisExceptionCheckedFunction.java +++ /dev/null @@ -1,24 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.managers; - -import glide.api.models.exceptions.RedisException; - -/** - * Functional Interface to convert values and throw RedisException when encountering an error state. - * - * @param type to evaluate - * @param payload type - */ -@FunctionalInterface -public interface RedisExceptionCheckedFunction { - - /** - * Functional response handler that takes a value of type R and returns a payload of type T. - * Throws RedisException when encountering an invalid or error state. - * - * @param value - received value type - * @return T - returning payload type - * @throws RedisException - */ - T apply(R value) throws RedisException; -} diff --git a/java/client/src/main/java/glide/utils/ArgsBuilder.java b/java/client/src/main/java/glide/utils/ArgsBuilder.java new file mode 100644 index 0000000000..066d75a707 --- /dev/null +++ b/java/client/src/main/java/glide/utils/ArgsBuilder.java @@ -0,0 +1,66 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.utils; + +import glide.api.models.GlideString; +import java.util.ArrayList; + +/** + * Helper class for collecting arbitrary type of arguments and stores them as an array of + * GlideString + */ +public class ArgsBuilder { + ArrayList argumentsList = null; + + public ArgsBuilder() { + argumentsList = new ArrayList<>(); + } + + public ArgsBuilder add(ArgType[] args) { + for (ArgType arg : args) { + argumentsList.add(GlideString.of(arg)); + } + + return this; + } + + public ArgsBuilder add(ArgType arg) { + argumentsList.add(GlideString.of(arg)); + return this; + } + + /** Append args to the list of argument only if condition is true */ + public ArgsBuilder addIf(ArgType[] args, boolean condition) { + if (condition) { + for (ArgType arg : args) { + argumentsList.add(GlideString.of(arg)); + } + } + return this; + } + + /** Append arg to the list of argument only if condition is true */ + public ArgsBuilder addIf(ArgType arg, boolean condition) { + if (condition) { + argumentsList.add(GlideString.of(arg)); + } + return this; + } + + public ArgsBuilder add(String[] args) { + for (String arg : args) { + argumentsList.add(GlideString.of(arg)); + } + return this; + } + + public ArgsBuilder add(int[] args) { + for (int arg : args) { + argumentsList.add(GlideString.of(arg)); + } + return this; + } + + public GlideString[] toArray() { + return argumentsList.toArray(new GlideString[0]); + } +} diff --git a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java index f3dda3130b..fccdac14f3 100644 --- a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java +++ b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java @@ -1,8 +1,16 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.utils; +import static glide.api.models.GlideString.gs; + +import glide.api.commands.GeospatialIndicesBaseCommands; +import glide.api.models.GlideString; +import glide.api.models.commands.geospatial.GeospatialData; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -11,18 +19,31 @@ public class ArrayTransformUtils { /** - * Converts a map of string keys and values of any type in to an array of strings with alternating - * keys and values. + * Converts a map of string keys and values of any type that can be converted in to an array of + * strings with alternating keys and values. * * @param args Map of string keys to values of any type to convert. * @return Array of strings [key1, value1.toString(), key2, value2.toString(), ...]. */ public static String[] convertMapToKeyValueStringArray(Map args) { return args.entrySet().stream() - .flatMap(entry -> Stream.of(entry.getKey(), entry.getValue().toString())) + .flatMap(entry -> Stream.of(entry.getKey(), entry.getValue())) .toArray(String[]::new); } + /** + * Converts a map of GlideString keys and values of any type in to an array of GlideStrings with + * alternating keys and values. + * + * @param args Map of GlideString keys to values of any type to convert. + * @return Array of strings [key1, gs(value1.toString()), key2, gs(value2.toString()), ...]. + */ + public static GlideString[] convertMapToKeyValueGlideStringArray(Map args) { + return args.entrySet().stream() + .flatMap(entry -> Stream.of(entry.getKey(), GlideString.gs(entry.getValue().toString()))) + .toArray(GlideString[]::new); + } + /** * Converts a map of string keys and values of any type into an array of strings with alternating * values and keys. @@ -36,6 +57,56 @@ public static String[] convertMapToValueKeyStringArray(Map args) { .toArray(String[]::new); } + /** + * Converts a map of GlideString keys and values of any type into an array of GlideStrings with + * alternating values and keys. + * + * @param args Map of GlideString keys to values of any type to convert. + * @return Array of GlideStrings [gs(value1.toString()), key1, gs(value2.toString()), key2, ...]. + */ + public static GlideString[] convertMapToValueKeyStringArrayBinary(Map args) { + return args.entrySet().stream() + .flatMap(entry -> Stream.of(gs(entry.getValue().toString()), entry.getKey())) + .toArray(GlideString[]::new); + } + + /** + * Converts a geospatial members to geospatial data mapping in to an array of arguments in the + * form of [Longitude, Latitude, Member ...]. + * + * @param args A mapping of member names to their corresponding positions. + * @return An array of strings to be used in {@link GeospatialIndicesBaseCommands#geoadd}. + */ + public static String[] mapGeoDataToArray(Map args) { + return args.entrySet().stream() + .flatMap( + entry -> + Stream.of( + Double.toString(entry.getValue().getLongitude()), + Double.toString(entry.getValue().getLatitude()), + entry.getKey())) + .toArray(String[]::new); + } + + /** + * Converts a geospatial members to geospatial data mapping in to an array of arguments in the + * form of [Longitude, Latitude, Member ...]. + * + * @param args A mapping of member names to their corresponding positions. + * @return An array of GlideStrings to be used in {@link GeospatialIndicesBaseCommands#geoadd}. + */ + public static GlideString[] mapGeoDataToGlideStringArray( + Map args) { + return args.entrySet().stream() + .flatMap( + entry -> + Stream.of( + GlideString.of(entry.getValue().getLongitude()), + GlideString.of(entry.getValue().getLatitude()), + GlideString.of(entry.getKey()))) + .toArray(GlideString[]::new); + } + /** * Casts an array of objects to an array of type T. * @@ -55,20 +126,102 @@ public static U[] castArray(T[] objectArr, Class clazz) { .toArray(size -> (U[]) Array.newInstance(clazz, size)); } + /** + * Casts an Object[][] to T[][] by casting each nested array and every + * array element. + * + * @param outerObjectArr Array of arrays of objects to cast. + * @param clazz The class of the array elements to cast to. + * @return An array of arrays of type U, containing the elements from the input array. + * @param The base type from which the elements are being cast. + * @param The subtype of T to which the elements are cast. + */ + @SuppressWarnings("unchecked") + public static U[][] castArrayofArrays(T[] outerObjectArr, Class clazz) { + if (outerObjectArr == null) { + return null; + } + T[] convertedArr = (T[]) new Object[outerObjectArr.length]; + for (int i = 0; i < outerObjectArr.length; i++) { + convertedArr[i] = (T) castArray((T[]) outerObjectArr[i], clazz); + } + return (U[][]) castArray(convertedArr, Array.newInstance(clazz, 0).getClass()); + } + + /** + * Casts an Object[][][] to T[][][] by casting each nested array and + * every array element. + * + * @param outerObjectArr 3D array of objects to cast. + * @param clazz The class of the array elements to cast to. + * @return An array of arrays of type U, containing the elements from the input array. + * @param The base type from which the elements are being cast. + * @param The subtype of T to which the elements are cast. + */ + public static U[][][] cast3DArray(T[] outerObjectArr, Class clazz) { + if (outerObjectArr == null) { + return null; + } + T[] convertedArr = (T[]) new Object[outerObjectArr.length]; + for (int i = 0; i < outerObjectArr.length; i++) { + convertedArr[i] = (T) castArrayofArrays((T[]) outerObjectArr[i], clazz); + } + return (U[][][]) castArrayofArrays(convertedArr, Array.newInstance(clazz, 0).getClass()); + } + /** * Maps a Map of Arrays with value type T[] to value of U[]. * * @param mapOfArrays Map of Array values to cast. * @param clazz The class of the array values to cast to. * @return A Map of arrays of type U[], containing the key/values from the input Map. - * @param The base type from which the elements are being cast. - * @param The subtype of T to which the elements are cast. + * @param The target type which the elements are cast. */ - @SuppressWarnings("unchecked") - public static Map castMapOfArrays( - Map mapOfArrays, Class clazz) { + public static Map castMapOfArrays( + Map mapOfArrays, Class clazz) { + if (mapOfArrays == null) { + return null; + } return mapOfArrays.entrySet().stream() - .collect(Collectors.toMap(k -> k.getKey(), e -> castArray(e.getValue(), clazz))); + .collect(Collectors.toMap(Map.Entry::getKey, e -> castArray(e.getValue(), clazz))); + } + + /** + * Maps a Map of Arrays with value type T[] to value of U[]. + * + * @param mapOfArrays Map of Array values to cast. + * @param clazz The class of the array values to cast to. + * @return A Map of arrays of type U[], containing the key/values from the input Map. + * @param The target type which the elements are cast. + */ + public static Map castBinaryStringMapOfArrays( + Map mapOfArrays, Class clazz) { + if (mapOfArrays == null) { + return null; + } + return mapOfArrays.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> castArray(e.getValue(), clazz))); + } + + /** + * Maps a Map of Object[][] with value type T[][] to value of U[][]. + * + * @param mapOfArrays Map of 2D Array values to cast. + * @param clazz The class of the array values to cast to. + * @return A Map of arrays of type U[][], containing the key/values from the input Map. + * @param The target type which the elements are cast. + * @param String type, could be either {@link String} or {@link GlideString}. + */ + public static Map castMapOf2DArray( + Map mapOfArrays, Class clazz) { + if (mapOfArrays == null) { + return null; + } + return mapOfArrays.entrySet().stream() + .collect( + HashMap::new, + (m, e) -> m.put(e.getKey(), castArrayofArrays(e.getValue(), clazz)), + HashMap::putAll); } /** @@ -82,4 +235,82 @@ public static Map castMapOfArrays( public static T[] concatenateArrays(T[]... arrays) { return Stream.of(arrays).flatMap(Stream::of).toArray(size -> Arrays.copyOf(arrays[0], size)); } + + /** + * Converts a map of any type of keys and values in to an array of GlideString with alternating + * keys and values. + * + * @param args Map of keys to values of any type to convert. + * @return Array of GlideString [key1, value1, key2, value2, ...]. + */ + public static GlideString[] flattenMapToGlideStringArray(Map args) { + return args.entrySet().stream() + .flatMap( + entry -> Stream.of(GlideString.of(entry.getKey()), GlideString.of(entry.getValue()))) + .toArray(GlideString[]::new); + } + + /** + * Converts a map of any type of keys and values in to an array of GlideString with alternating + * values and keys. + * + *

This method is similar to flattenMapToGlideStringArray, but it places the value before the + * key + * + * @param args Map of keys to values of any type to convert. + * @return Array of GlideString [value1, key1, value2, key2...]. + */ + public static GlideString[] flattenMapToGlideStringArrayValueFirst(Map args) { + return args.entrySet().stream() + .flatMap( + entry -> Stream.of(GlideString.of(entry.getValue()), GlideString.of(entry.getKey()))) + .toArray(GlideString[]::new); + } + + /** + * Converts a map of any type of keys and values in to an array of GlideString where all keys are + * placed first, followed by the values. + * + * @param args Map of keys to values of any type to convert. + * @return Array of GlideString [key1, key2, value1, value2...]. + */ + public static GlideString[] flattenAllKeysFollowedByAllValues(Map args) { + List keysList = new ArrayList<>(); + List valuesList = new ArrayList<>(); + + for (var entry : args.entrySet()) { + keysList.add(GlideString.of(entry.getKey())); + valuesList.add(GlideString.of(entry.getValue())); + } + + return concatenateArrays( + keysList.toArray(GlideString[]::new), valuesList.toArray(GlideString[]::new)); + } + + /** + * Converts any array into GlideString array keys and values. + * + * @param args Map of keys to values of any type to convert. + * @return Array of strings [key1, value1, key2, value2, ...]. + */ + public static GlideString[] toGlideStringArray(ArgType[] args) { + return Arrays.stream(args).map(GlideString::of).toArray(GlideString[]::new); + } + + /** + * Given an inputMap of any key / value pairs, create a new Map of + * + * @param inputMap Map of values to convert. + * @return A Map of + */ + public static Map convertMapToGlideStringMap(Map inputMap) { + if (inputMap == null) { + return null; + } + return inputMap.entrySet().stream() + .collect( + HashMap::new, + (m, e) -> m.put(GlideString.of(e.getKey()), GlideString.of(e.getValue())), + HashMap::putAll); + } } diff --git a/java/client/src/main/java/module-info.java b/java/client/src/main/java/module-info.java index 7453dea352..99c4655082 100644 --- a/java/client/src/main/java/module-info.java +++ b/java/client/src/main/java/module-info.java @@ -1,8 +1,14 @@ module glide.api { exports glide.api; exports glide.api.commands; + exports glide.api.logging; exports glide.api.models; exports glide.api.models.commands; + exports glide.api.models.commands.bitmap; + exports glide.api.models.commands.geospatial; + exports glide.api.models.commands.function; + exports glide.api.models.commands.scan; + exports glide.api.models.commands.stream; exports glide.api.models.configuration; exports glide.api.models.exceptions; diff --git a/java/client/src/test/java/glide/ExceptionHandlingTests.java b/java/client/src/test/java/glide/ExceptionHandlingTests.java index 43bdb224d0..7f6ebe0ec2 100644 --- a/java/client/src/test/java/glide/ExceptionHandlingTests.java +++ b/java/client/src/test/java/glide/ExceptionHandlingTests.java @@ -1,30 +1,33 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; +import static command_request.CommandRequestOuterClass.RequestType.CustomCommand; import static glide.ffi.resolvers.SocketListenerResolver.getSocket; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import static org.mockito.Mockito.mockStatic; import static response.ResponseOuterClass.RequestErrorType.Disconnect; import static response.ResponseOuterClass.RequestErrorType.ExecAbort; import static response.ResponseOuterClass.RequestErrorType.Timeout; import static response.ResponseOuterClass.RequestErrorType.Unspecified; +import command_request.CommandRequestOuterClass.CommandRequest; import connection_request.ConnectionRequestOuterClass; -import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.logging.Logger; +import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.ConnectionException; import glide.api.models.exceptions.ExecAbortException; -import glide.api.models.exceptions.RedisException; +import glide.api.models.exceptions.GlideException; import glide.api.models.exceptions.RequestException; import glide.api.models.exceptions.TimeoutException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; import glide.connectors.resources.Platform; import glide.connectors.resources.ThreadPoolResourceAllocator; -import glide.managers.BaseCommandResponseResolver; +import glide.managers.BaseResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; import io.netty.channel.ChannelFuture; @@ -33,21 +36,24 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.stream.Stream; -import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import redis_request.RedisRequestOuterClass.RedisRequest; +import org.mockito.MockedStatic; import response.ResponseOuterClass.RequestError; import response.ResponseOuterClass.RequestErrorType; import response.ResponseOuterClass.Response; public class ExceptionHandlingTests { + private MockedStatic mockedLogger; + @BeforeEach public void init() { + mockedLogger = mockStatic(Logger.class); var threadPoolResource = ThreadPoolResourceAllocator.getOrCreate(() -> null); if (threadPoolResource != null) { threadPoolResource.getEventLoopGroup().shutdownGracefully(); @@ -55,6 +61,11 @@ public void init() { } } + @AfterEach + public void teardown() { + mockedLogger.close(); + } + /** * This test shows how exception handling works in the middle of future pipeline The client has * similar stuff, but it rethrows an exception. @@ -77,7 +88,7 @@ public void channel_is_closed_when_failed_to_connect() { var callbackDispatcher = new TestCallbackDispatcher(new ClosingException("TEST")); var channelHandler = new TestChannelHandler(callbackDispatcher); var connectionManager = new ConnectionManager(channelHandler); - var future = connectionManager.connectToRedis(createDummyConfig()); + var future = connectionManager.connectToValkey(createDummyConfig()); callbackDispatcher.completeRequest(null); var exception = assertThrows(ExecutionException.class, future::get); @@ -123,7 +134,7 @@ public void channel_is_not_closed_when_error_was_in_command_pipeline() { @Test @SneakyThrows - public void command_manager_rethrows_non_RedisException_too() { + public void command_manager_rethrows_non_GlideException_too() { var callbackDispatcher = new TestCallbackDispatcher(new IOException("TEST")); var channelHandler = new TestChannelHandler(callbackDispatcher); var commandManager = new CommandManager(channelHandler); @@ -141,12 +152,12 @@ public void command_manager_rethrows_non_RedisException_too() { @Test @SneakyThrows - public void connection_manager_rethrows_non_RedisException_too() { + public void connection_manager_rethrows_non_GlideException_too() { var callbackDispatcher = new TestCallbackDispatcher(new IOException("TEST")); var channelHandler = new TestChannelHandler(callbackDispatcher); var connectionManager = new ConnectionManager(channelHandler); - var future = connectionManager.connectToRedis(createDummyConfig()); + var future = connectionManager.connectToValkey(createDummyConfig()); callbackDispatcher.completeRequest(null); var exception = assertThrows(ExecutionException.class, future::get); @@ -163,12 +174,12 @@ public void connection_manager_rethrows_non_RedisException_too() { public void close_connection_on_response_with_closing_error() { // CallbackDispatcher throws ClosingException which causes ConnectionManager and CommandManager // to close the channel - var callbackDispatcher = new CallbackDispatcher(); + var callbackDispatcher = new CallbackDispatcher(null); var channelHandler = new TestChannelHandler(callbackDispatcher); var connectionManager = new ConnectionManager(channelHandler); - var future1 = connectionManager.connectToRedis(createDummyConfig()); - var future2 = connectionManager.connectToRedis(createDummyConfig()); + var future1 = connectionManager.connectToValkey(createDummyConfig()); + var future2 = connectionManager.connectToValkey(createDummyConfig()); var response = Response.newBuilder().setClosingError("TEST").build(); callbackDispatcher.completeRequest(response); @@ -204,8 +215,8 @@ private static Stream getProtobufErrorsToJavaClientErrorsMapping() { public void dont_close_connection_when_callback_dispatcher_receives_response_with_closing_error( // CallbackDispatcher throws a corresponding exception which should not cause // ConnectionManager and CommandManager to close the channel - RequestErrorType errorType, Class exceptionType) { - var callbackDispatcher = new CallbackDispatcher(); + RequestErrorType errorType, Class exceptionType) { + var callbackDispatcher = new CallbackDispatcher(null); var channelHandler = new TestChannelHandler(callbackDispatcher); var commandManager = new CommandManager(channelHandler); @@ -218,7 +229,7 @@ public void dont_close_connection_when_callback_dispatcher_receives_response_wit callbackDispatcher.completeRequest(response); var exception = assertThrows(ExecutionException.class, future::get); - // a RedisException thrown from CallbackDispatcher::completeRequest and then + // a GlideException thrown from CallbackDispatcher::completeRequest and then // rethrown by CommandManager::exceptionHandler assertEquals(exceptionType, exception.getCause().getClass()); assertEquals("TEST", exception.getCause().getMessage()); @@ -230,12 +241,12 @@ public void dont_close_connection_when_callback_dispatcher_receives_response_wit @SneakyThrows public void close_connection_on_response_without_error_but_with_incorrect_callback_id() { // CallbackDispatcher does the same as it received closing error - var callbackDispatcher = new CallbackDispatcher(); + var callbackDispatcher = new CallbackDispatcher(null); var channelHandler = new TestChannelHandler(callbackDispatcher); var connectionManager = new ConnectionManager(channelHandler); - var future1 = connectionManager.connectToRedis(createDummyConfig()); - var future2 = connectionManager.connectToRedis(createDummyConfig()); + var future1 = connectionManager.connectToValkey(createDummyConfig()); + var future2 = connectionManager.connectToValkey(createDummyConfig()); var response = Response.newBuilder().setCallbackIdx(42).build(); callbackDispatcher.completeRequest(response); @@ -260,7 +271,7 @@ public void close_connection_on_response_without_error_but_with_incorrect_callba @Test public void response_resolver_does_not_expect_errors() { - var resolver = new BaseCommandResponseResolver(null); + var resolver = new BaseResponseResolver(null); var response1 = Response.newBuilder() @@ -274,9 +285,11 @@ public void response_resolver_does_not_expect_errors() { assertEquals("Unhandled response closing error", exception.getMessage()); } + // TODO add tests for error handling in MessageHandler + /** Create a config which causes connection failure. */ - private static RedisClientConfiguration createDummyConfig() { - return RedisClientConfiguration.builder().build(); + private static GlideClientConfiguration createDummyConfig() { + return GlideClientConfiguration.builder().build(); } /** Test ChannelHandler extension which allows to validate whether the channel was closed. */ @@ -298,7 +311,7 @@ public ChannelFuture close() { } @Override - public CompletableFuture write(RedisRequest.Builder request, boolean flush) { + public CompletableFuture write(CommandRequest.Builder request, boolean flush) { var commandId = callbackDispatcher.registerRequest(); return commandId.getValue(); } @@ -311,11 +324,15 @@ public CompletableFuture connect( } /** Test ChannelHandler extension which aborts futures for all commands. */ - @RequiredArgsConstructor private static class TestCallbackDispatcher extends CallbackDispatcher { public final Throwable exceptionToThrow; + private TestCallbackDispatcher(Throwable exceptionToThrow) { + super(null); + this.exceptionToThrow = exceptionToThrow; + } + @Override public void completeRequest(Response response) { responses.values().forEach(future -> future.completeExceptionally(exceptionToThrow)); diff --git a/java/client/src/test/java/glide/api/RedisClientCreateTest.java b/java/client/src/test/java/glide/api/GlideClientCreateTest.java similarity index 60% rename from java/client/src/test/java/glide/api/RedisClientCreateTest.java rename to java/client/src/test/java/glide/api/GlideClientCreateTest.java index 04ab6d2a8a..ff8b14854d 100644 --- a/java/client/src/test/java/glide/api/RedisClientCreateTest.java +++ b/java/client/src/test/java/glide/api/GlideClientCreateTest.java @@ -1,10 +1,11 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; import static glide.api.BaseClient.buildChannelHandler; -import static glide.api.RedisClient.CreateClient; -import static glide.api.RedisClient.buildCommandManager; -import static glide.api.RedisClient.buildConnectionManager; +import static glide.api.BaseClient.buildMessageHandler; +import static glide.api.GlideClient.buildCommandManager; +import static glide.api.GlideClient.buildConnectionManager; +import static glide.api.GlideClient.createClient; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -13,9 +14,10 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; -import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.ChannelHandler; +import glide.connectors.handlers.MessageHandler; import glide.connectors.resources.ThreadPoolResource; import glide.connectors.resources.ThreadPoolResourceAllocator; import glide.managers.CommandManager; @@ -28,12 +30,13 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -public class RedisClientCreateTest { +public class GlideClientCreateTest { private MockedStatic mockedClient; private ChannelHandler channelHandler; private ConnectionManager connectionManager; private CommandManager commandManager; + private MessageHandler messageHandler; private ThreadPoolResource threadPoolResource; @BeforeEach @@ -43,12 +46,14 @@ public void init() { channelHandler = mock(ChannelHandler.class); commandManager = mock(CommandManager.class); connectionManager = mock(ConnectionManager.class); + messageHandler = mock(MessageHandler.class); threadPoolResource = mock(ThreadPoolResource.class); - mockedClient.when(() -> buildChannelHandler(any())).thenReturn(channelHandler); + mockedClient.when(() -> buildChannelHandler(any(), any())).thenReturn(channelHandler); mockedClient.when(() -> buildConnectionManager(channelHandler)).thenReturn(connectionManager); mockedClient.when(() -> buildCommandManager(channelHandler)).thenReturn(commandManager); - mockedClient.when(() -> CreateClient(any(), any())).thenCallRealMethod(); + mockedClient.when(() -> buildMessageHandler(any())).thenReturn(messageHandler); + mockedClient.when(() -> createClient(any(), any())).thenCallRealMethod(); var threadPoolResource = ThreadPoolResourceAllocator.getOrCreate(() -> null); if (threadPoolResource != null) { @@ -64,17 +69,17 @@ public void teardown() { @Test @SneakyThrows - public void createClient_with_default_config_successfully_returns_RedisClient() { + public void createClient_with_default_config_successfully_returns_GlideClient() { // setup - CompletableFuture connectToRedisFuture = new CompletableFuture<>(); - connectToRedisFuture.complete(null); - RedisClientConfiguration config = RedisClientConfiguration.builder().build(); + CompletableFuture connectToValkeyFuture = new CompletableFuture<>(); + connectToValkeyFuture.complete(null); + GlideClientConfiguration config = GlideClientConfiguration.builder().build(); - when(connectionManager.connectToRedis(eq(config))).thenReturn(connectToRedisFuture); + when(connectionManager.connectToValkey(eq(config))).thenReturn(connectToValkeyFuture); // exercise - CompletableFuture result = CreateClient(config); - RedisClient client = result.get(); + CompletableFuture result = createClient(config); + GlideClient client = result.get(); // verify assertEquals(connectionManager, client.connectionManager); @@ -83,18 +88,18 @@ public void createClient_with_default_config_successfully_returns_RedisClient() @Test @SneakyThrows - public void createClient_with_custom_config_successfully_returns_RedisClient() { + public void createClient_with_custom_config_successfully_returns_GlideClient() { // setup - CompletableFuture connectToRedisFuture = new CompletableFuture<>(); - connectToRedisFuture.complete(null); - RedisClientConfiguration config = - RedisClientConfiguration.builder().threadPoolResource(threadPoolResource).build(); + CompletableFuture connectToValkeyFuture = new CompletableFuture<>(); + connectToValkeyFuture.complete(null); + GlideClientConfiguration config = + GlideClientConfiguration.builder().threadPoolResource(threadPoolResource).build(); - when(connectionManager.connectToRedis(eq(config))).thenReturn(connectToRedisFuture); + when(connectionManager.connectToValkey(eq(config))).thenReturn(connectToValkeyFuture); // exercise - CompletableFuture result = CreateClient(config); - RedisClient client = result.get(); + CompletableFuture result = createClient(config); + GlideClient client = result.get(); // verify assertEquals(connectionManager, client.connectionManager); @@ -105,20 +110,22 @@ public void createClient_with_custom_config_successfully_returns_RedisClient() { @Test public void createClient_error_on_connection_throws_ExecutionException() { // setup - CompletableFuture connectToRedisFuture = new CompletableFuture<>(); + CompletableFuture connectToValkeyFuture = new CompletableFuture<>(); ClosingException exception = new ClosingException("disconnected"); - connectToRedisFuture.completeExceptionally(exception); - RedisClientConfiguration config = - RedisClientConfiguration.builder().threadPoolResource(threadPoolResource).build(); + connectToValkeyFuture.completeExceptionally(exception); + GlideClientConfiguration config = + GlideClientConfiguration.builder().threadPoolResource(threadPoolResource).build(); - when(connectionManager.connectToRedis(eq(config))).thenReturn(connectToRedisFuture); + when(connectionManager.connectToValkey(eq(config))).thenReturn(connectToValkeyFuture); // exercise - CompletableFuture result = CreateClient(config); + CompletableFuture result = createClient(config); ExecutionException executionException = assertThrows(ExecutionException.class, result::get); // verify assertEquals(exception, executionException.getCause()); } + + // TODO check message queue and subscriptionConfiguration } diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java new file mode 100644 index 0000000000..2727db7346 --- /dev/null +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -0,0 +1,15284 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api; + +import static command_request.CommandRequestOuterClass.RequestType.Append; +import static command_request.CommandRequestOuterClass.RequestType.BLMPop; +import static command_request.CommandRequestOuterClass.RequestType.BLMove; +import static command_request.CommandRequestOuterClass.RequestType.BLPop; +import static command_request.CommandRequestOuterClass.RequestType.BRPop; +import static command_request.CommandRequestOuterClass.RequestType.BZMPop; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.BitCount; +import static command_request.CommandRequestOuterClass.RequestType.BitField; +import static command_request.CommandRequestOuterClass.RequestType.BitFieldReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.BitOp; +import static command_request.CommandRequestOuterClass.RequestType.BitPos; +import static command_request.CommandRequestOuterClass.RequestType.ClientGetName; +import static command_request.CommandRequestOuterClass.RequestType.ClientId; +import static command_request.CommandRequestOuterClass.RequestType.ConfigGet; +import static command_request.CommandRequestOuterClass.RequestType.ConfigResetStat; +import static command_request.CommandRequestOuterClass.RequestType.ConfigRewrite; +import static command_request.CommandRequestOuterClass.RequestType.ConfigSet; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.CustomCommand; +import static command_request.CommandRequestOuterClass.RequestType.DBSize; +import static command_request.CommandRequestOuterClass.RequestType.Decr; +import static command_request.CommandRequestOuterClass.RequestType.DecrBy; +import static command_request.CommandRequestOuterClass.RequestType.Del; +import static command_request.CommandRequestOuterClass.RequestType.Dump; +import static command_request.CommandRequestOuterClass.RequestType.Echo; +import static command_request.CommandRequestOuterClass.RequestType.Exists; +import static command_request.CommandRequestOuterClass.RequestType.Expire; +import static command_request.CommandRequestOuterClass.RequestType.ExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.ExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.FCall; +import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.FlushAll; +import static command_request.CommandRequestOuterClass.RequestType.FlushDB; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDelete; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDump; +import static command_request.CommandRequestOuterClass.RequestType.FunctionFlush; +import static command_request.CommandRequestOuterClass.RequestType.FunctionKill; +import static command_request.CommandRequestOuterClass.RequestType.FunctionList; +import static command_request.CommandRequestOuterClass.RequestType.FunctionLoad; +import static command_request.CommandRequestOuterClass.RequestType.FunctionRestore; +import static command_request.CommandRequestOuterClass.RequestType.FunctionStats; +import static command_request.CommandRequestOuterClass.RequestType.GeoAdd; +import static command_request.CommandRequestOuterClass.RequestType.GeoDist; +import static command_request.CommandRequestOuterClass.RequestType.GeoHash; +import static command_request.CommandRequestOuterClass.RequestType.GeoPos; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearch; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearchStore; +import static command_request.CommandRequestOuterClass.RequestType.Get; +import static command_request.CommandRequestOuterClass.RequestType.GetBit; +import static command_request.CommandRequestOuterClass.RequestType.GetDel; +import static command_request.CommandRequestOuterClass.RequestType.GetEx; +import static command_request.CommandRequestOuterClass.RequestType.GetRange; +import static command_request.CommandRequestOuterClass.RequestType.HDel; +import static command_request.CommandRequestOuterClass.RequestType.HExists; +import static command_request.CommandRequestOuterClass.RequestType.HGet; +import static command_request.CommandRequestOuterClass.RequestType.HGetAll; +import static command_request.CommandRequestOuterClass.RequestType.HIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.HIncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.HKeys; +import static command_request.CommandRequestOuterClass.RequestType.HLen; +import static command_request.CommandRequestOuterClass.RequestType.HMGet; +import static command_request.CommandRequestOuterClass.RequestType.HRandField; +import static command_request.CommandRequestOuterClass.RequestType.HScan; +import static command_request.CommandRequestOuterClass.RequestType.HSet; +import static command_request.CommandRequestOuterClass.RequestType.HSetNX; +import static command_request.CommandRequestOuterClass.RequestType.HStrlen; +import static command_request.CommandRequestOuterClass.RequestType.HVals; +import static command_request.CommandRequestOuterClass.RequestType.Incr; +import static command_request.CommandRequestOuterClass.RequestType.IncrBy; +import static command_request.CommandRequestOuterClass.RequestType.IncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.Info; +import static command_request.CommandRequestOuterClass.RequestType.LCS; +import static command_request.CommandRequestOuterClass.RequestType.LIndex; +import static command_request.CommandRequestOuterClass.RequestType.LInsert; +import static command_request.CommandRequestOuterClass.RequestType.LLen; +import static command_request.CommandRequestOuterClass.RequestType.LMPop; +import static command_request.CommandRequestOuterClass.RequestType.LMove; +import static command_request.CommandRequestOuterClass.RequestType.LPop; +import static command_request.CommandRequestOuterClass.RequestType.LPos; +import static command_request.CommandRequestOuterClass.RequestType.LPush; +import static command_request.CommandRequestOuterClass.RequestType.LPushX; +import static command_request.CommandRequestOuterClass.RequestType.LRange; +import static command_request.CommandRequestOuterClass.RequestType.LRem; +import static command_request.CommandRequestOuterClass.RequestType.LSet; +import static command_request.CommandRequestOuterClass.RequestType.LTrim; +import static command_request.CommandRequestOuterClass.RequestType.LastSave; +import static command_request.CommandRequestOuterClass.RequestType.Lolwut; +import static command_request.CommandRequestOuterClass.RequestType.MGet; +import static command_request.CommandRequestOuterClass.RequestType.MSet; +import static command_request.CommandRequestOuterClass.RequestType.MSetNX; +import static command_request.CommandRequestOuterClass.RequestType.Move; +import static command_request.CommandRequestOuterClass.RequestType.ObjectEncoding; +import static command_request.CommandRequestOuterClass.RequestType.ObjectFreq; +import static command_request.CommandRequestOuterClass.RequestType.ObjectIdleTime; +import static command_request.CommandRequestOuterClass.RequestType.ObjectRefCount; +import static command_request.CommandRequestOuterClass.RequestType.PExpire; +import static command_request.CommandRequestOuterClass.RequestType.PExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.PExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.PTTL; +import static command_request.CommandRequestOuterClass.RequestType.Persist; +import static command_request.CommandRequestOuterClass.RequestType.PfAdd; +import static command_request.CommandRequestOuterClass.RequestType.PfCount; +import static command_request.CommandRequestOuterClass.RequestType.PfMerge; +import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.Publish; +import static command_request.CommandRequestOuterClass.RequestType.RPop; +import static command_request.CommandRequestOuterClass.RequestType.RPush; +import static command_request.CommandRequestOuterClass.RequestType.RPushX; +import static command_request.CommandRequestOuterClass.RequestType.RandomKey; +import static command_request.CommandRequestOuterClass.RequestType.Rename; +import static command_request.CommandRequestOuterClass.RequestType.RenameNX; +import static command_request.CommandRequestOuterClass.RequestType.Restore; +import static command_request.CommandRequestOuterClass.RequestType.SAdd; +import static command_request.CommandRequestOuterClass.RequestType.SCard; +import static command_request.CommandRequestOuterClass.RequestType.SDiff; +import static command_request.CommandRequestOuterClass.RequestType.SDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.SInter; +import static command_request.CommandRequestOuterClass.RequestType.SInterCard; +import static command_request.CommandRequestOuterClass.RequestType.SInterStore; +import static command_request.CommandRequestOuterClass.RequestType.SIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMembers; +import static command_request.CommandRequestOuterClass.RequestType.SMove; +import static command_request.CommandRequestOuterClass.RequestType.SPop; +import static command_request.CommandRequestOuterClass.RequestType.SRandMember; +import static command_request.CommandRequestOuterClass.RequestType.SRem; +import static command_request.CommandRequestOuterClass.RequestType.SScan; +import static command_request.CommandRequestOuterClass.RequestType.SUnion; +import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; +import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.Select; +import static command_request.CommandRequestOuterClass.RequestType.SetBit; +import static command_request.CommandRequestOuterClass.RequestType.SetRange; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Strlen; +import static command_request.CommandRequestOuterClass.RequestType.TTL; +import static command_request.CommandRequestOuterClass.RequestType.Time; +import static command_request.CommandRequestOuterClass.RequestType.Touch; +import static command_request.CommandRequestOuterClass.RequestType.Type; +import static command_request.CommandRequestOuterClass.RequestType.UnWatch; +import static command_request.CommandRequestOuterClass.RequestType.Unlink; +import static command_request.CommandRequestOuterClass.RequestType.Wait; +import static command_request.CommandRequestOuterClass.RequestType.Watch; +import static command_request.CommandRequestOuterClass.RequestType.XAck; +import static command_request.CommandRequestOuterClass.RequestType.XAdd; +import static command_request.CommandRequestOuterClass.RequestType.XAutoClaim; +import static command_request.CommandRequestOuterClass.RequestType.XClaim; +import static command_request.CommandRequestOuterClass.RequestType.XDel; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreate; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreateConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDelConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDestroy; +import static command_request.CommandRequestOuterClass.RequestType.XGroupSetId; +import static command_request.CommandRequestOuterClass.RequestType.XInfoConsumers; +import static command_request.CommandRequestOuterClass.RequestType.XInfoGroups; +import static command_request.CommandRequestOuterClass.RequestType.XInfoStream; +import static command_request.CommandRequestOuterClass.RequestType.XLen; +import static command_request.CommandRequestOuterClass.RequestType.XPending; +import static command_request.CommandRequestOuterClass.RequestType.XRange; +import static command_request.CommandRequestOuterClass.RequestType.XRead; +import static command_request.CommandRequestOuterClass.RequestType.XReadGroup; +import static command_request.CommandRequestOuterClass.RequestType.XRevRange; +import static command_request.CommandRequestOuterClass.RequestType.XTrim; +import static command_request.CommandRequestOuterClass.RequestType.ZAdd; +import static command_request.CommandRequestOuterClass.RequestType.ZCard; +import static command_request.CommandRequestOuterClass.RequestType.ZCount; +import static command_request.CommandRequestOuterClass.RequestType.ZDiff; +import static command_request.CommandRequestOuterClass.RequestType.ZDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.ZIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.ZInter; +import static command_request.CommandRequestOuterClass.RequestType.ZInterCard; +import static command_request.CommandRequestOuterClass.RequestType.ZInterStore; +import static command_request.CommandRequestOuterClass.RequestType.ZLexCount; +import static command_request.CommandRequestOuterClass.RequestType.ZMPop; +import static command_request.CommandRequestOuterClass.RequestType.ZMScore; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.ZRandMember; +import static command_request.CommandRequestOuterClass.RequestType.ZRange; +import static command_request.CommandRequestOuterClass.RequestType.ZRangeStore; +import static command_request.CommandRequestOuterClass.RequestType.ZRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRem; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByLex; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByScore; +import static command_request.CommandRequestOuterClass.RequestType.ZRevRank; +import static command_request.CommandRequestOuterClass.RequestType.ZScan; +import static command_request.CommandRequestOuterClass.RequestType.ZScore; +import static command_request.CommandRequestOuterClass.RequestType.ZUnion; +import static command_request.CommandRequestOuterClass.RequestType.ZUnionStore; +import static glide.api.BaseClient.OK; +import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; +import static glide.api.commands.GenericCommands.DB_VALKEY_API; +import static glide.api.commands.HashBaseCommands.WITH_VALUES_VALKEY_API; +import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_VALKEY_API; +import static glide.api.commands.ServerManagementCommands.VERSION_VALKEY_API; +import static glide.api.commands.SetBaseCommands.SET_LIMIT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.COUNT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.LIMIT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_VALKEY_API; +import static glide.api.commands.StringBaseCommands.IDX_COMMAND_STRING; +import static glide.api.commands.StringBaseCommands.LCS_MATCHES_RESULT_KEY; +import static glide.api.commands.StringBaseCommands.LEN_VALKEY_API; +import static glide.api.commands.StringBaseCommands.MINMATCHLEN_COMMAND_STRING; +import static glide.api.commands.StringBaseCommands.WITHMATCHLEN_COMMAND_STRING; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; +import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; +import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; +import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; +import static glide.api.models.commands.SortOptionsBinary.BY_COMMAND_GLIDE_STRING; +import static glide.api.models.commands.SortOptionsBinary.GET_COMMAND_GLIDE_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl.SAT; +import static glide.api.models.commands.bitmap.BitFieldOptions.GET_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.INCRBY_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.OVERFLOW_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.SET_COMMAND_STRING; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_VALKEY_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_VALKEY_API; +import static glide.api.models.commands.geospatial.GeoAddOptions.CHANGED_VALKEY_API; +import static glide.api.models.commands.geospatial.GeoSearchOrigin.FROMLONLAT_VALKEY_API; +import static glide.api.models.commands.geospatial.GeoSearchOrigin.FROMMEMBER_VALKEY_API; +import static glide.api.models.commands.scan.BaseScanOptions.COUNT_OPTION_STRING; +import static glide.api.models.commands.scan.BaseScanOptions.MATCH_OPTION_STRING; +import static glide.api.models.commands.scan.BaseScanOptionsBinary.COUNT_OPTION_GLIDE_STRING; +import static glide.api.models.commands.scan.BaseScanOptionsBinary.MATCH_OPTION_GLIDE_STRING; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.STRING; +import static glide.api.models.commands.scan.ScanOptions.TYPE_OPTION_STRING; +import static glide.api.models.commands.stream.StreamAddOptions.NO_MAKE_STREAM_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.FORCE_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.IDLE_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.JUST_ID_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.RETRY_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.TIME_VALKEY_API; +import static glide.api.models.commands.stream.StreamGroupOptions.ENTRIES_READ_VALKEY_API; +import static glide.api.models.commands.stream.StreamGroupOptions.MAKE_STREAM_VALKEY_API; +import static glide.api.models.commands.stream.StreamPendingOptions.IDLE_TIME_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.EXCLUSIVE_RANGE_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.MAXIMUM_RANGE_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.MINIMUM_RANGE_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.RANGE_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadGroupOptions.READ_GROUP_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadGroupOptions.READ_NOACK_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_BLOCK_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_STREAMS_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_EXACT_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_LIMIT_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_MAXLEN_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_MINID_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_NOT_EXACT_VALKEY_API; +import static glide.api.models.commands.stream.XInfoStreamOptions.COUNT; +import static glide.api.models.commands.stream.XInfoStreamOptions.FULL; +import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static glide.utils.ArrayTransformUtils.convertMapToKeyValueGlideStringArray; +import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; +import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; +import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArrayBinary; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import command_request.CommandRequestOuterClass.RequestType; +import glide.api.models.GlideString; +import glide.api.models.Script; +import glide.api.models.Transaction; +import glide.api.models.commands.ConditionalChange; +import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.GetExOptions; +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; +import glide.api.models.commands.RangeOptions; +import glide.api.models.commands.RangeOptions.InfLexBound; +import glide.api.models.commands.RangeOptions.InfScoreBound; +import glide.api.models.commands.RangeOptions.LexBoundary; +import glide.api.models.commands.RangeOptions.RangeByIndex; +import glide.api.models.commands.RangeOptions.RangeByLex; +import glide.api.models.commands.RangeOptions.RangeByScore; +import glide.api.models.commands.RangeOptions.ScoreBoundary; +import glide.api.models.commands.RestoreOptions; +import glide.api.models.commands.ScoreFilter; +import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; +import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SetOptions.Expiry; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.SortOptionsBinary; +import glide.api.models.commands.SortOrder; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.WeightAggregateOptions.KeyArrayBinary; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeysBinary; +import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.function.FunctionLoadOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.geospatial.GeoAddOptions; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.HScanOptionsBinary; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.SScanOptionsBinary; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.scan.ZScanOptionsBinary; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamAddOptionsBinary; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamPendingOptions; +import glide.api.models.commands.stream.StreamPendingOptionsBinary; +import glide.api.models.commands.stream.StreamRange; +import glide.api.models.commands.stream.StreamRange.IdBound; +import glide.api.models.commands.stream.StreamRange.InfRangeBound; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions; +import glide.api.models.commands.stream.StreamTrimOptions.MaxLen; +import glide.api.models.commands.stream.StreamTrimOptions.MinId; +import glide.managers.CommandManager; +import glide.utils.ArgsBuilder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class GlideClientTest { + + // bypass import conflict between Set (collection) and Set (enum variant) + private static final RequestType pSet = RequestType.Set; + + GlideClient service; + + CommandManager commandManager; + + @BeforeEach + public void setUp() { + commandManager = mock(CommandManager.class); + service = new GlideClient(new BaseClient.ClientBuilder(null, commandManager, null, null)); + } + + @SneakyThrows + @Test + public void customCommand_returns_success() { + // setup + String key = "testKey"; + Object value = "testValue"; + String cmd = "GETSTRING"; + String[] arguments = new String[] {cmd, key}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(CustomCommand), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.customCommand(arguments); + String payload = (String) response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void exec() { + // setup + Object[] value = new Object[] {"PONG", "PONG"}; + Transaction transaction = new Transaction().ping().ping(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewTransaction(eq(transaction), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.exec(transaction); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void echo_returns_success() { + // setup + String message = "Valkey GLIDE"; + String[] arguments = new String[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Echo), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.echo(message); + String echo = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, echo); + } + + @SneakyThrows + @Test + public void echo_binary_returns_success() { + // setup + GlideString message = gs("Valkey GLIDE"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Echo), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.echo(message); + GlideString echo = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, echo); + } + + @SneakyThrows + @Test + public void ping_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete("PONG"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals("PONG", payload); + } + + @SneakyThrows + @Test + public void ping_with_message_returns_success() { + // setup + String message = "RETURN OF THE PONG"; + String[] arguments = new String[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + String pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void ping_binary_with_message_returns_success() { + // setup + GlideString message = gs("RETURN OF THE PONG"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + GlideString pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void select_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + long index = 5L; + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Select), eq(new String[] {Long.toString(index)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.select(index); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void del_returns_long_success() { + // setup + String[] keys = new String[] {"testKey1", "testKey2"}; + Long numberDeleted = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(numberDeleted); + when(commandManager.submitNewCommand(eq(Del), eq(keys), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.del(keys); + Long result = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(numberDeleted, result); + } + + @SneakyThrows + @Test + public void del_returns_long_success_binary() { + // setup + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + Long numberDeleted = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(numberDeleted); + when(commandManager.submitNewCommand(eq(Del), eq(keys), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.del(keys); + Long result = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(numberDeleted, result); + } + + @SneakyThrows + @Test + public void unlink_returns_long_success() { + // setup + String[] keys = new String[] {"testKey1", "testKey2"}; + Long numberUnlinked = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(numberUnlinked); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Unlink), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.unlink(keys); + Long result = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(numberUnlinked, result); + } + + @SneakyThrows + @Test + public void unlink_binary_returns_long_success() { + // setup + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + Long numberUnlinked = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(numberUnlinked); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Unlink), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.unlink(keys); + Long result = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(numberUnlinked, result); + } + + @SneakyThrows + @Test + public void get_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(Get), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.get(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getdel() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(GetDel), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getdel(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getex() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(GetEx), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getex(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getex_binary() { + // setup + GlideString key = gs("testKey"); + GlideString value = gs("testValue"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand( + eq(GetEx), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getex(key); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getex_with_options() { + // setup + String key = "testKey"; + String value = "testValue"; + GetExOptions options = GetExOptions.Seconds(10L); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand( + eq(GetEx), eq(new String[] {key, "EX", "10"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getex(key, options); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getex_with_options_binary() { + // setup + GlideString key = gs("testKey"); + GlideString value = gs("testValue"); + GetExOptions options = GetExOptions.Seconds(10L); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand( + eq(GetEx), eq(new GlideString[] {key, gs("EX"), gs("10")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getex(key, options); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + private static List getGetExOptions() { + return List.of( + Arguments.of( + // seconds + "test_with_seconds", GetExOptions.Seconds(10L), new String[] {"EX", "10"}), + Arguments.of( + // milliseconds + "test_with_milliseconds", + GetExOptions.Milliseconds(1000L), + new String[] {"PX", "1000"}), + Arguments.of( + // unix seconds + "test_with_unix_seconds", GetExOptions.UnixSeconds(10L), new String[] {"EXAT", "10"}), + Arguments.of( + // unix milliseconds + "test_with_unix_milliseconds", + GetExOptions.UnixMilliseconds(1000L), + new String[] {"PXAT", "1000"}), + Arguments.of( + // persist + "test_with_persist", GetExOptions.Persist(), new String[] {"PERSIST"})); + } + + private static List getGetExOptionsBinary() { + return List.of( + Arguments.of( + // seconds + "test_with_seconds", GetExOptions.Seconds(10L), new GlideString[] {gs("EX"), gs("10")}), + Arguments.of( + // milliseconds + "test_with_milliseconds", + GetExOptions.Milliseconds(1000L), + new GlideString[] {gs("PX"), gs("1000")}), + Arguments.of( + // unix seconds + "test_with_unix_seconds", + GetExOptions.UnixSeconds(10L), + new GlideString[] {gs("EXAT"), gs("10")}), + Arguments.of( + // unix milliseconds + "test_with_unix_milliseconds", + GetExOptions.UnixMilliseconds(1000L), + new GlideString[] {gs("PXAT"), gs("1000")}), + Arguments.of( + // persist + "test_with_persist", GetExOptions.Persist(), new GlideString[] {gs("PERSIST")})); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("getGetExOptions") + public void getex_options(String testName, GetExOptions options, String[] expectedArgs) { + assertArrayEquals( + expectedArgs, options.toArgs(), "Expected " + testName + " toArgs() to pass."); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("getGetExOptionsBinary") + public void getex_options_binary( + String testName, GetExOptions options, GlideString[] expectedGlideStringArgs) { + assertArrayEquals( + expectedGlideStringArgs, + options.toGlideStringArgs(), + "Expected " + testName + " toArgs() to pass."); + } + + @SneakyThrows + @Test + public void set_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(null); + when(commandManager.submitNewCommand(eq(pSet), eq(new String[] {key, value}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value); + Object okResponse = response.get(); + + // verify + assertEquals(testResponse, response); + assertNull(okResponse); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfExists_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + SetOptions setOptions = + SetOptions.builder() + .conditionalSet(ONLY_IF_EXISTS) + .returnOldValue(false) + .expiry(Expiry.KeepExisting()) + .build(); + String[] arguments = new String[] {key, value, ONLY_IF_EXISTS.getValkeyApi(), "KEEPTTL"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(null); + when(commandManager.submitNewCommand(eq(pSet), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value, setOptions); + + // verify + assertEquals(testResponse, response); + assertNull(response.get()); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { + // setup + String key = "testKey"; + String value = "testValue"; + SetOptions setOptions = + SetOptions.builder() + .conditionalSet(ONLY_IF_DOES_NOT_EXIST) + .returnOldValue(true) + .expiry(Expiry.UnixSeconds(60L)) + .build(); + String[] arguments = + new String[] { + key, value, ONLY_IF_DOES_NOT_EXIST.getValkeyApi(), RETURN_OLD_VALUE, "EXAT", "60" + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(pSet), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.set(key, value, setOptions); + + // verify + assertNotNull(response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void exists_returns_long_success() { + // setup + String[] keys = new String[] {"testKey1", "testKey2"}; + Long numberExisting = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(numberExisting); + when(commandManager.submitNewCommand(eq(Exists), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.exists(keys); + Long result = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(numberExisting, result); + } + + @SneakyThrows + @Test + public void exists_binary_returns_long_success() { + // setup + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + Long numberExisting = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(numberExisting); + when(commandManager.submitNewCommand(eq(Exists), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.exists(keys); + Long result = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(numberExisting, result); + } + + @SneakyThrows + @Test + public void expire_returns_success() { + // setup + String key = "testKey"; + long seconds = 10L; + String[] arguments = new String[] {key, Long.toString(seconds)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expire_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long seconds = 10L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(seconds))}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expire_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long seconds = 10L; + String[] arguments = new String[] {key, Long.toString(seconds), "NX"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds, ExpireOptions.HAS_NO_EXPIRY); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void expire_with_expireOptions_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long seconds = 10L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(seconds)), gs("NX")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds, ExpireOptions.HAS_NO_EXPIRY); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_returns_success() { + // setup + String key = "testKey"; + long unixSeconds = 100000L; + String[] arguments = new String[] {key, Long.toString(unixSeconds)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expireAt(key, unixSeconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long unixSeconds = 100000L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(unixSeconds))}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expireAt(key, unixSeconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long unixSeconds = 100000L; + String[] arguments = new String[] {key, Long.toString(unixSeconds), "XX"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.expireAt(key, unixSeconds, ExpireOptions.HAS_EXISTING_EXPIRY); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_with_expireOptions_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long unixSeconds = 100000L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(unixSeconds)), gs("XX")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.expireAt(key, unixSeconds, ExpireOptions.HAS_EXISTING_EXPIRY); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_returns_success() { + // setup + String key = "testKey"; + long milliseconds = 50000L; + String[] arguments = new String[] {key, Long.toString(milliseconds)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpire(key, milliseconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long milliseconds = 50000L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(milliseconds))}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpire(key, milliseconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long milliseconds = 50000L; + String[] arguments = new String[] {key, Long.toString(milliseconds), "LT"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpire(key, milliseconds, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_with_expireOptions_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long milliseconds = 50000L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(milliseconds)), gs("LT")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpire(key, milliseconds, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_returns_success() { + // setup + String key = "testKey"; + long unixMilliseconds = 999999L; + String[] arguments = new String[] {key, Long.toString(unixMilliseconds)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpireAt(key, unixMilliseconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long unixMilliseconds = 999999L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(unixMilliseconds))}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpireAt(key, unixMilliseconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long unixMilliseconds = 999999L; + String[] arguments = new String[] {key, Long.toString(unixMilliseconds), "GT"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpireAt(key, unixMilliseconds, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_with_expireOptions_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long unixMilliseconds = 999999L; + GlideString[] arguments = + new GlideString[] {key, gs(Long.toString(unixMilliseconds)), gs("GT")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.FALSE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpireAt(key, unixMilliseconds, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void ttl_returns_success() { + // setup + String key = "testKey"; + long ttl = 999L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(ttl); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(TTL), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ttl(key); + + // verify + assertEquals(testResponse, response); + assertEquals(ttl, response.get()); + } + + @SneakyThrows + @Test + public void ttl_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long ttl = 999L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(ttl); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(TTL), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ttl(key); + + // verify + assertEquals(testResponse, response); + assertEquals(ttl, response.get()); + } + + @SneakyThrows + @Test + public void expiretime_returns_success() { + // setup + String key = "testKey"; + long value = 999L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireTime), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expiretime(key); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void expiretime_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long value = 999L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireTime), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expiretime(key); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void pexpiretime_returns_success() { + // setup + String key = "testKey"; + long value = 999L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireTime), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpiretime(key); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void pexpiretime_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long value = 999L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireTime), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpiretime(key); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + String payload = "hello"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript(eq(script), eq(List.of()), eq(List.of()), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_with_ScriptOptions_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + String payload = "hello"; + + ScriptOptions options = + ScriptOptions.builder().key("key1").key("key2").arg("arg1").arg("arg2").build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript( + eq(script), + eq(List.of(gs("key1"), gs("key2"))), + eq(List.of(gs("arg1"), gs("arg2"))), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, options); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void invokeScript_with_ScriptOptionsGlideString_returns_success() { + // setup + Script script = mock(Script.class); + String hash = UUID.randomUUID().toString(); + when(script.getHash()).thenReturn(hash); + GlideString payload = gs("hello"); + + ScriptOptionsGlideString options = + ScriptOptionsGlideString.builder() + .key(gs("key1")) + .key(gs("key2")) + .arg(gs("arg1")) + .arg(gs("arg2")) + .build(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitScript( + eq(script), + eq(List.of(gs("key1"), gs("key2"))), + eq(List.of(gs("arg1"), gs("arg2"))), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.invokeScript(script, options); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void pttl_returns_success() { + // setup + String key = "testKey"; + long pttl = 999000L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(pttl); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PTTL), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pttl(key); + + // verify + assertEquals(testResponse, response); + assertEquals(pttl, response.get()); + } + + @SneakyThrows + @Test + public void pttl_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long pttl = 999000L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(pttl); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PTTL), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pttl(key); + + // verify + assertEquals(testResponse, response); + assertEquals(pttl, response.get()); + } + + @SneakyThrows + @Test + public void persist_returns_success() { + // setup + String key = "testKey"; + Boolean isTimeoutRemoved = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(isTimeoutRemoved); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Persist), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.persist(key); + + // verify + assertEquals(testResponse, response); + assertEquals(isTimeoutRemoved, response.get()); + } + + @SneakyThrows + @Test + public void persist_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Boolean isTimeoutRemoved = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(isTimeoutRemoved); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Persist), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.persist(key); + + // verify + assertEquals(testResponse, response); + assertEquals(isTimeoutRemoved, response.get()); + } + + @SneakyThrows + @Test + public void info_returns_success() { + // setup + String testPayload = "Key: Value"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(testPayload); + when(commandManager.submitNewCommand(eq(Info), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.info(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void info_with_multiple_InfoOptions_returns_success() { + // setup + String[] arguments = + new String[] {InfoOptions.Section.ALL.toString(), InfoOptions.Section.DEFAULT.toString()}; + String testPayload = "Key: Value"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(testPayload); + when(commandManager.submitNewCommand(eq(Info), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + InfoOptions options = + InfoOptions.builder() + .section(InfoOptions.Section.ALL) + .section(InfoOptions.Section.DEFAULT) + .build(); + CompletableFuture response = service.info(options); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void info_with_empty_InfoOptions_returns_success() { + // setup + String testPayload = "Key: Value"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(testPayload); + when(commandManager.submitNewCommand(eq(Info), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.info(InfoOptions.builder().build()); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void mget_returns_success() { + // setup + String[] keys = {"key1", null, "key2"}; + String[] values = {"value1", null, "value2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(values); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MGet), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.mget(keys); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(values, payload); + } + + @SneakyThrows + @Test + public void mset_returns_success() { + // setup + Map keyValueMap = new LinkedHashMap<>(); + keyValueMap.put("key1", "value1"); + keyValueMap.put("key2", "value2"); + String[] args = {"key1", "value1", "key2", "value2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MSet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.mset(keyValueMap); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void mset_binary_returns_success() { + // setup + Map keyValueMap = new LinkedHashMap<>(); + keyValueMap.put(gs("key1"), gs("value1")); + keyValueMap.put(gs("key2"), gs("value2")); + GlideString[] args = {gs("key1"), gs("value1"), gs("key2"), gs("value2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MSet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.msetBinary(keyValueMap); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void msetnx_returns_success() { + // setup + Map keyValueMap = new LinkedHashMap<>(); + keyValueMap.put("key1", "value1"); + keyValueMap.put("key2", "value2"); + String[] args = {"key1", "value1", "key2", "value2"}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MSetNX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.msetnx(keyValueMap); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void msetnx_binary_returns_success() { + // setup + Map keyValueMap = new LinkedHashMap<>(); + keyValueMap.put(gs("key1"), gs("value1")); + keyValueMap.put(gs("key2"), gs("value2")); + GlideString[] args = {gs("key1"), gs("value1"), gs("key2"), gs("value2")}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(MSetNX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.msetnxBinary(keyValueMap); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incr_returns_success() { + // setup + String key = "testKey"; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Incr), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incr(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incr_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Incr), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incr(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incrBy_returns_success() { + // setup + String key = "testKey"; + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrBy), eq(new String[] {key, Long.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrBy(key, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incrBy_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrBy), eq(new GlideString[] {key, gs(Long.toString(amount).getBytes())}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrBy(key, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incrByFloat_returns_success() { + // setup + String key = "testKey"; + double amount = 1.1; + Double value = 10.1; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrByFloat), eq(new String[] {key, Double.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrByFloat(key, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void incrByFloat_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + double amount = 1.1; + Double value = 10.1; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrByFloat), + eq(new GlideString[] {key, gs(Double.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrByFloat(key, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void decr_returns_success() { + // setup + String key = "testKey"; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Decr), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.decr(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void decr_returns_success_binary() { + // setup + String key = "testKey"; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Decr), eq(new GlideString[] {gs(key)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.decr(gs(key)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void decrBy_returns_success() { + // setup + String key = "testKey"; + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(DecrBy), eq(new String[] {key, Long.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.decrBy(key, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void decrBy_returns_success_binary() { + // setup + String key = "testKey"; + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(DecrBy), eq(new GlideString[] {gs(key), gs(Long.toString(amount))}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.decrBy(gs(key), amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void strlen_returns_success() { + // setup + String key = "testKey"; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Strlen), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.strlen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void setrange_returns_success() { + // setup + String key = "testKey"; + int offset = 42; + String str = "pewpew"; + String[] arguments = new String[] {key, Integer.toString(offset), str}; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SetRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.setrange(key, offset, str); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void setrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + int offset = 42; + GlideString str = gs("pewpew"); + GlideString[] arguments = new GlideString[] {key, gs(Integer.toString(offset)), str}; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SetRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.setrange(key, offset, str); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getrange_returns_success() { + // setup + String key = "testKey"; + int start = 42; + int end = 54; + String[] arguments = new String[] {key, Integer.toString(start), Integer.toString(end)}; + String value = "pewpew"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GetRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getrange(key, start, end); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + int start = 42; + int end = 54; + GlideString[] arguments = + new GlideString[] {key, gs(Integer.toString(start)), gs(Integer.toString(end))}; + GlideString value = gs("pewpew"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GetRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getrange(key, start, end); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hget_success() { + // setup + String key = "testKey"; + String field = "field"; + String[] args = new String[] {key, field}; + String value = "value"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(HGet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hget(key, field); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hget_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + GlideString[] args = new GlideString[] {key, field}; + GlideString value = gs("value"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(HGet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hget(key, field); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hset_success() { + // setup + String key = "testKey"; + Map fieldValueMap = new LinkedHashMap<>(); + fieldValueMap.put("field1", "value1"); + fieldValueMap.put("field2", "value2"); + String[] args = new String[] {key, "field1", "value1", "field2", "value2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(HSet), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hset(key, fieldValueMap); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hset_binary_success() { + // setup + GlideString key = gs("testKey"); + Map fieldValueMap = new LinkedHashMap<>(); + fieldValueMap.put(gs("field1"), gs("value1")); + fieldValueMap.put(gs("field2"), gs("value2")); + GlideString[] args = + new GlideString[] {key, gs("field1"), gs("value1"), gs("field2"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + when(commandManager.submitNewCommand(eq(HSet), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hset(key, fieldValueMap); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hsetnx_success() { + // setup + String key = "testKey"; + String field = "testField"; + String value = "testValue"; + String[] args = new String[] {key, field, value}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HSetNX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hsetnx(key, field, value); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertTrue(payload); + } + + @SneakyThrows + @Test + public void hsetnx_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("testField"); + GlideString value = gs("testValue"); + GlideString[] args = new GlideString[] {key, field, value}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HSetNX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hsetnx(key, field, value); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertTrue(payload); + } + + @SneakyThrows + @Test + public void hdel_success() { + // setup + String key = "testKey"; + String[] fields = {"testField1", "testField2"}; + String[] args = {key, "testField1", "testField2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HDel), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hdel(key, fields); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hdel_success_binary() { + // setup + GlideString key = gs("testKey"); + GlideString[] fields = {gs("testField1"), gs("testField2")}; + GlideString[] args = {key, gs("testField1"), gs("testField2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HDel), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hdel(key, fields); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hlen_success() { + // setup + String key = "testKey"; + String[] args = {key}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HLen), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hlen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hlen_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] args = {key}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HLen), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hlen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hvals_success() { + // setup + String key = "testKey"; + String[] args = {key}; + String[] values = new String[] {"value1", "value2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(values); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HVals), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hvals(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(values, payload); + } + + @SneakyThrows + @Test + public void hvals_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] args = {key}; + GlideString[] values = new GlideString[] {gs("value1"), gs("value2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(values); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HVals), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hvals(key); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(values, payload); + } + + @SneakyThrows + @Test + public void hmget_success() { + // setup + String key = "testKey"; + String[] fields = {"testField1", "testField2"}; + String[] args = {"testKey", "testField1", "testField2"}; + String[] value = {"testValue1", "testValue2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HMGet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hmget(key, fields); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hmget_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] fields = {gs("testField1"), gs("testField2")}; + GlideString[] args = {gs("testKey"), gs("testField1"), gs("testField2")}; + GlideString[] value = {gs("testValue1"), gs("testValue2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HMGet), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hmget(key, fields); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hexists_success() { + // setup + String key = "testKey"; + String field = "testField"; + String[] args = new String[] {key, field}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HExists), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hexists(key, field); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hexists_binary_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("testField"); + GlideString[] args = new GlideString[] {key, field}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HExists), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hexists(key, field); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hgetall_success() { + // setup + String key = "testKey"; + String[] args = new String[] {key}; + Map value = new LinkedHashMap<>(); + value.put("key1", "field1"); + value.put("key2", "field2"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(HGetAll), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.hgetall(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hincrBy_returns_success() { + // setup + String key = "testKey"; + String field = "field"; + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrBy), eq(new String[] {key, field, Long.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrBy(key, field, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hincrBy_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrBy), + eq(new GlideString[] {key, field, gs(Long.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrBy(key, field, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hincrByFloat_returns_success() { + // setup + String key = "testKey"; + String field = "field"; + double amount = 1.0; + Double value = 10.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrByFloat), eq(new String[] {key, field, Double.toString(amount)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrByFloat(key, field, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hincrByFloat_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + double amount = 1.0; + Double value = 10.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrByFloat), + eq(new GlideString[] {key, field, gs(Double.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrByFloat(key, field, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hkeys_returns_success() { + // setup + String key = "testKey"; + String[] args = {key}; + String[] values = new String[] {"field_1", "field_2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(values); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HKeys), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hkeys(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(values, payload); + } + + @SneakyThrows + @Test + public void hkeys_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] args = {key}; + GlideString[] values = new GlideString[] {gs("field_1"), gs("field_2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(values); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HKeys), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hkeys(key); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(values, payload); + } + + @SneakyThrows + @Test + public void hstrlen_returns_success() { + // setup + String key = "testKey"; + String field = "field"; + String[] args = {key, field}; + Long value = 42L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HStrlen), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hstrlen(key, field); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hrandfield_returns_success() { + // setup + String key = "testKey"; + String[] args = {key}; + String field = "field"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(field); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HRandField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hrandfield(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(field, payload); + } + + @SneakyThrows + @Test + public void hrandfieldWithCount_returns_success() { + // setup + String key = "testKey"; + String[] args = {key, "2"}; + String[] fields = new String[] {"field_1", "field_2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(fields); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HRandField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hrandfieldWithCount(key, 2); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(fields, payload); + } + + @SneakyThrows + @Test + public void hrandfieldWithCountWithValues_returns_success() { + // setup + String key = "testKey"; + String[] args = {key, "2", WITH_VALUES_VALKEY_API}; + String[][] fields = new String[][] {{"field_1", "value_1"}, {"field_2", "value_2"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(fields); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HRandField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hrandfieldWithCountWithValues(key, 2); + String[][] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(fields, payload); + } + + @SneakyThrows + @Test + public void lpush_returns_success() { + // setup + String key = "testKey"; + String[] elements = new String[] {"value1", "value2"}; + String[] args = new String[] {key, "value1", "value2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpush_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpop_returns_success() { + // setup + String key = "testKey"; + String[] args = new String[] {key}; + String value = "value"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpop(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpop_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] args = new GlideString[] {key}; + GlideString value = gs("value"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpop(key); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpopCount_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] args = new String[] {key, Long.toString(count)}; + String[] value = new String[] {"value1", "value2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpopCount(key, count); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpopCount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString[] args = new GlideString[] {key, gs(Long.toString(count))}; + GlideString[] value = new GlideString[] {gs("value1"), gs("value2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpopCount(key, count); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpos() { + // setup + String[] args = new String[] {"list", "element"}; + long index = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpos("list", "element"); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(index, payload); + } + + @SneakyThrows + @Test + public void lpos_binary() { + // setup + GlideString[] args = new GlideString[] {gs("list"), gs("element")}; + long index = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpos(gs("list"), gs("element")); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(index, payload); + } + + @SneakyThrows + @Test + public void lpos_withOptions() { + // setup + LPosOptions options = LPosOptions.builder().rank(1L).maxLength(1000L).build(); + String[] args = new String[] {"list", "element", "RANK", "1", "MAXLEN", "1000"}; + long index = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpos("list", "element", options); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(index, payload); + } + + @SneakyThrows + @Test + public void lpos_withOptions_binary() { + // setup + LPosOptions options = LPosOptions.builder().rank(1L).maxLength(1000L).build(); + GlideString[] args = + new GlideString[] { + gs("list"), gs("element"), gs("RANK"), gs("1"), gs("MAXLEN"), gs("1000") + }; + long index = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpos(gs("list"), gs("element"), options); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(index, payload); + } + + @SneakyThrows + @Test + public void lposCount() { + // setup + String[] args = new String[] {"list", "element", "COUNT", "1"}; + Long[] index = new Long[] {1L}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lposCount("list", "element", 1L); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(index, payload); + } + + @SneakyThrows + @Test + public void lposCount_binary() { + // setup + GlideString[] args = new GlideString[] {gs("list"), gs("element"), gs("COUNT"), gs("1")}; + Long[] index = new Long[] {1L}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lposCount(gs("list"), gs("element"), 1L); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(index, payload); + } + + @SneakyThrows + @Test + public void lposCount_withOptions() { + // setup + LPosOptions options = LPosOptions.builder().rank(1L).maxLength(1000L).build(); + String[] args = new String[] {"list", "element", "COUNT", "0", "RANK", "1", "MAXLEN", "1000"}; + Long[] index = new Long[] {0L}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lposCount("list", "element", 0L, options); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(index, payload); + } + + @SneakyThrows + @Test + public void lposCount_withOptions_binary() { + // setup + LPosOptions options = LPosOptions.builder().rank(1L).maxLength(1000L).build(); + GlideString[] args = + new GlideString[] { + gs("list"), + gs("element"), + gs("COUNT"), + gs("0"), + gs("RANK"), + gs("1"), + gs("MAXLEN"), + gs("1000") + }; + Long[] index = new Long[] {0L}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(index); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPos), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lposCount(gs("list"), gs("element"), 0L, options); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(index, payload); + } + + @SneakyThrows + @Test + public void lrange_returns_success() { + // setup + String key = "testKey"; + long start = 2L; + long end = 4L; + String[] args = new String[] {key, Long.toString(start), Long.toString(end)}; + String[] value = new String[] {"value1", "value2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRange), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrange(key, start, end); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long start = 2L; + long end = 4L; + GlideString[] args = new GlideString[] {key, gs(Long.toString(start)), gs(Long.toString(end))}; + GlideString[] value = new GlideString[] {gs("value1"), gs("value2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRange), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrange(key, start, end); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lindex_returns_success() { + // setup + String key = "testKey"; + long index = 2; + String[] args = new String[] {key, Long.toString(index)}; + String value = "value"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LIndex), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lindex(key, index); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lindex_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long index = 2; + GlideString[] args = new GlideString[] {key, gs(Long.toString(index))}; + GlideString value = gs("value"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LIndex), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lindex(key, index); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void ltrim_returns_success() { + // setup + String key = "testKey"; + long start = 2L; + long end = 2L; + String[] args = new String[] {key, Long.toString(end), Long.toString(start)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LTrim), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ltrim(key, start, end); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void ltrim_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long start = 2L; + long end = 2L; + GlideString[] args = new GlideString[] {key, gs(Long.toString(end)), gs(Long.toString(start))}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LTrim), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ltrim(key, start, end); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void llen_returns_success() { + // setup + String key = "testKey"; + String[] args = new String[] {key}; + long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LLen), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.llen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lrem_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String element = "value"; + String[] args = new String[] {key, Long.toString(count), element}; + long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRem), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrem(key, count, element); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lrem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString element = gs("value"); + GlideString[] args = new GlideString[] {key, gs(Long.toString(count).getBytes()), element}; + long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRem), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrem(key, count, element); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpush_returns_success() { + // setup + String key = "testKey"; + String[] elements = new String[] {"value1", "value2"}; + String[] args = new String[] {key, "value1", "value2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpush_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpop_returns_success() { + // setup + String key = "testKey"; + String value = "value"; + String[] args = new String[] {key}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpop(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpop_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString value = gs("value"); + GlideString[] args = new GlideString[] {key}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpop(key); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpopCount_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] args = new String[] {key, Long.toString(count)}; + String[] value = new String[] {"value1", "value2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpopCount(key, count); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpopCount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString[] args = new GlideString[] {key, gs(Long.toString(count))}; + GlideString[] value = new GlideString[] {gs("value1"), gs("value2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPop), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpopCount(key, count); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sadd_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"testMember1", "testMember2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sadd(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sadd_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = new GlideString[] {gs("testMember1"), gs("testMember2")}; + GlideString[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sadd(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sismember_returns_success() { + // setup + String key = "testKey"; + String member = "testMember"; + String[] arguments = new String[] {key, member}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SIsMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sismember(key, member); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertTrue(payload); + } + + @SneakyThrows + @Test + public void sismember_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SIsMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sismember(key, member); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertTrue(payload); + } + + @SneakyThrows + @Test + public void srem_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"testMember1", "testMember2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void srem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = new GlideString[] {gs("testMember1"), gs("testMember2")}; + GlideString[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void smembers_returns_success() { + // setup + String key = "testKey"; + Set value = Set.of("testMember"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SMembers), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.smembers(key); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scard_returns_success() { + // setup + String key = "testKey"; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SCard), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scard_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SCard), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sdiff_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + Set value = Set.of("1", "2"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SDiff), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sdiff(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sdiff_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Set value = Set.of(gs("1"), gs("2")); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SDiff), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sdiff(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void smismember_returns_success() { + // setup + String key = "testKey"; + String[] members = {"1", "2"}; + String[] arguments = {"testKey", "1", "2"}; + Boolean[] value = {true, false}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SMIsMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.smismember(key, members); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void smismember_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = {gs("1"), gs("2")}; + GlideString[] arguments = {gs("testKey"), gs("1"), gs("2")}; + Boolean[] value = {true, false}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SMIsMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.smismember(key, members); + Boolean[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sdiffstore_returns_success() { + // setup + String destination = "dest"; + String[] keys = new String[] {"set1", "set2"}; + String[] arguments = {"dest", "set1", "set2"}; + + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SDiffStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sdiffstore(destination, keys); + + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sdiffstore_binary_returns_success() { + // setup + GlideString destination = gs("dest"); + GlideString[] keys = new GlideString[] {gs("set1"), gs("set2")}; + GlideString[] arguments = {gs("dest"), gs("set1"), gs("set2")}; + + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SDiffStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sdiffstore(destination, keys); + + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void smove_returns_success() { + // setup + String source = "src"; + String destination = "dst"; + String member = "elem"; + String[] arguments = {source, destination, member}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.smove(source, destination, member); + + // verify + assertEquals(testResponse, response); + assertTrue(response.get()); + } + + @SneakyThrows + @Test + public void smove_binary_returns_success() { + // setup + GlideString source = gs("src"); + GlideString destination = gs("dst"); + GlideString member = gs("elem"); + GlideString[] arguments = {source, destination, member}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.smove(source, destination, member); + + // verify + assertEquals(testResponse, response); + assertTrue(response.get()); + } + + @SneakyThrows + @Test + public void sinter_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + Set value = Set.of("1", "2"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SInter), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sinter(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sinter_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Set value = Set.of(gs("1"), gs("2")); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SInter), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sinter(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sinterstore_returns_success() { + // setup + String destination = "key"; + String[] keys = new String[] {"set1", "set2"}; + String[] args = new String[] {"key", "set1", "set2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SInterStore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sinterstore(destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sinterstore_binary_returns_success() { + // setup + GlideString destination = gs("key"); + GlideString[] keys = new GlideString[] {gs("set1"), gs("set2")}; + GlideString[] args = new GlideString[] {gs("key"), gs("set1"), gs("set2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SInterStore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sinterstore(destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sunionstore_returns_success() { + // setup + String destination = "key"; + String[] keys = new String[] {"set1", "set2"}; + String[] args = new String[] {"key", "set1", "set2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SUnionStore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sunionstore(destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sunionstore_binary_returns_success() { + // setup + GlideString destination = gs("key"); + GlideString[] keys = new GlideString[] {gs("set1"), gs("set2")}; + GlideString[] args = new GlideString[] {gs("key"), gs("set1"), gs("set2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SUnionStore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sunionstore(destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zadd_noOptions_returns_success() { + // setup + String key = "testKey"; + Map membersScores = new LinkedHashMap<>(); + membersScores.put("testMember1", 1.0); + membersScores.put("testMember2", 2.0); + String[] membersScoresArgs = convertMapToValueKeyStringArray(membersScores); + String[] arguments = ArrayUtils.addFirst(membersScoresArgs, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zadd(key, membersScores, ZAddOptions.builder().build(), false); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zadd_binary_noOptions_returns_success() { + // setup + GlideString key = gs("testKey"); + Map membersScores = new LinkedHashMap<>(); + membersScores.put(gs("testMember1"), 1.0); + membersScores.put(gs("testMember2"), 2.0); + GlideString[] membersScoresArgs = convertMapToValueKeyStringArrayBinary(membersScores); + GlideString[] arguments = ArrayUtils.addFirst(membersScoresArgs, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zadd(key, membersScores, ZAddOptions.builder().build(), false); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zadd_withOptions_returns_success() { + // setup + String key = "testKey"; + ZAddOptions options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + Map membersScores = new LinkedHashMap<>(); + membersScores.put("testMember1", 1.0); + membersScores.put("testMember2", 2.0); + String[] membersScoresArgs = convertMapToValueKeyStringArray(membersScores); + String[] arguments = ArrayUtils.addAll(new String[] {key}, options.toArgs()); + arguments = ArrayUtils.addAll(arguments, membersScoresArgs); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zadd(key, membersScores, options, false); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zadd_binary_withOptions_returns_success() { + // setup + GlideString key = gs("testKey"); + ZAddOptions options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + Map membersScores = new LinkedHashMap<>(); + membersScores.put(gs("testMember1"), 1.0); + membersScores.put(gs("testMember2"), 2.0); + GlideString[] membersScoresArgs = convertMapToValueKeyStringArrayBinary(membersScores); + GlideString[] arguments = ArrayUtils.addAll(new GlideString[] {key}, options.toArgsBinary()); + arguments = ArrayUtils.addAll(arguments, membersScoresArgs); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zadd(key, membersScores, options, false); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zadd_withIllegalArgument_throws_exception() { + // setup + String key = "testKey"; + ZAddOptions options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + Map membersScores = new LinkedHashMap<>(); + membersScores.put("testMember1", 1.0); + membersScores.put("testMember2", 2.0); + + assertThrows( + IllegalArgumentException.class, () -> service.zadd(key, membersScores, options, false)); + } + + @SneakyThrows + @Test + public void zadd_binary_withIllegalArgument_throws_exception() { + // setup + GlideString key = gs("testKey"); + ZAddOptions options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + Map membersScores = new LinkedHashMap<>(); + membersScores.put(gs("testMember1"), 1.0); + membersScores.put(gs("testMember2"), 2.0); + + assertThrows( + IllegalArgumentException.class, () -> service.zadd(key, membersScores, options, false)); + } + + @SneakyThrows + @Test + public void zaddIncr_noOptions_returns_success() { + // setup + String key = "testKey"; + String member = "member"; + double increment = 3.0; + String[] arguments = new String[] {key, "INCR", Double.toString(increment), member}; + Double value = 3.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zaddIncr(key, member, increment, ZAddOptions.builder().build()); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zaddIncr_binary_noOptions_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("member"); + double increment = 3.0; + GlideString[] arguments = + new GlideString[] {key, gs("INCR"), gs(Double.toString(increment)), member}; + Double value = 3.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zaddIncr(key, member, increment, ZAddOptions.builder().build()); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zaddIncr_withOptions_returns_success() { + // setup + String key = "testKey"; + ZAddOptions options = + ZAddOptions.builder() + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + String member = "member"; + double increment = 3.0; + String[] arguments = + concatenateArrays( + new String[] {key}, + options.toArgs(), + new String[] {"INCR", Double.toString(increment), member}); + Double value = 3.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zaddIncr(key, member, increment, options); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zaddIncr_binary_withOptions_returns_success() { + // setup + GlideString key = gs("testKey"); + ZAddOptions options = + ZAddOptions.builder() + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + GlideString member = gs("member"); + double increment = 3.0; + GlideString[] arguments = + concatenateArrays( + new GlideString[] {key}, + options.toArgsBinary(), + new GlideString[] {gs("INCR"), gs(Double.toString(increment)), member}); + Double value = 3.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zaddIncr(key, member, increment, options); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zmpop_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + ScoreFilter modifier = MAX; + String[] arguments = {"2", "key1", "key2", "MAX"}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zmpop(keys, modifier); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void zmpop_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + ScoreFilter modifier = MAX; + GlideString[] arguments = {gs("2"), gs("key1"), gs("key2"), gs("MAX")}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zmpop(keys, modifier); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void zmpop_with_count_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + ScoreFilter modifier = MAX; + long count = 42; + String[] arguments = {"2", "key1", "key2", "MAX", "COUNT", "42"}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zmpop(keys, modifier, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void zmpop_binary_with_count_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + ScoreFilter modifier = MAX; + long count = 42; + GlideString[] arguments = {gs("2"), gs("key1"), gs("key2"), gs("MAX"), gs("COUNT"), gs("42")}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zmpop(keys, modifier, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzmpop_returns_success() { + // setup + double timeout = .5; + String[] keys = new String[] {"key1", "key2"}; + ScoreFilter modifier = MAX; + String[] arguments = {"0.5", "2", "key1", "key2", "MAX"}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzmpop(keys, modifier, timeout); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzmpop_binary_returns_success() { + // setup + double timeout = .5; + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + ScoreFilter modifier = MAX; + GlideString[] arguments = {gs("0.5"), gs("2"), gs("key1"), gs("key2"), gs("MAX")}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzmpop(keys, modifier, timeout); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzmpop_with_count_returns_success() { + // setup + double timeout = .5; + String[] keys = new String[] {"key1", "key2"}; + ScoreFilter modifier = MAX; + long count = 42; + String[] arguments = {"0.5", "2", "key1", "key2", "MAX", "COUNT", "42"}; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzmpop(keys, modifier, timeout, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzmpop_binary_with_count_returns_success() { + // setup + double timeout = .5; + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + ScoreFilter modifier = MAX; + long count = 42; + GlideString[] arguments = { + gs("0.5"), gs("2"), gs("key1"), gs("key2"), gs("MAX"), gs("COUNT"), gs("42") + }; + Object[] value = new Object[] {"key1", "elem"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzmpop(keys, modifier, timeout, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void clientId_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(42L); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientId(); + + // verify + assertEquals(testResponse, response); + assertEquals(42L, response.get()); + } + + @SneakyThrows + @Test + public void clientGetName_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete("TEST"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientGetName(); + + // verify + assertEquals(testResponse, response); + assertEquals("TEST", response.get()); + } + + @SneakyThrows + @Test + public void configRewrite_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configGet_returns_success() { + // setup + Map testPayload = Map.of("timeout", "1000"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(testPayload); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ConfigGet), eq(new String[] {"timeout"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.configGet(new String[] {"timeout"}); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void configSet_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigSet), eq(new String[] {"timeout", "1000"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configSet(Map.of("timeout", "1000")); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void zrem_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"member1", "member2"}; + String[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = new GlideString[] {gs("member1"), gs("member2")}; + GlideString[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zcard_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zcard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zcard_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zcard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmin_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + Map value = Map.of("member1", 2.5); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZPopMin), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmin(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmin_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + Map value = Map.of(gs("member1"), 2.5); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZPopMin), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmin(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmin_with_count_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] arguments = new String[] {key, Long.toString(count)}; + Map value = Map.of("member1", 2.0, "member2", 3.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZPopMin), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmin(key, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmin_with_count_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + Map value = Map.of(gs("member1"), 2.0, gs("member2"), 3.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZPopMin), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmin(key, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzpopmin_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + double timeout = .5; + String[] arguments = new String[] {"key1", "key2", "0.5"}; + Object[] value = new Object[] {"key1", "elem", 42.}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZPopMin), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzpopmin(keys, timeout); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzpopmin_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + double timeout = .5; + GlideString[] arguments = new GlideString[] {gs("key1"), gs("key2"), gs("0.5")}; + Object[] value = new Object[] {"key1", "elem", 42.}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZPopMin), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzpopmin(keys, timeout); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmax_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + Map value = Map.of("member1", 2.5); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZPopMax), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmax(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmax_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + Map value = Map.of(gs("member1"), 2.5); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZPopMax), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmax(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzpopmax_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + double timeout = .5; + String[] arguments = new String[] {"key1", "key2", "0.5"}; + Object[] value = new Object[] {"key1", "elem", 42.}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZPopMax), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzpopmax(keys, timeout); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void bzpopmax_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + double timeout = .5; + GlideString[] arguments = new GlideString[] {gs("key1"), gs("key2"), gs("0.5")}; + Object[] value = new Object[] {"key1", "elem", 42.}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BZPopMax), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bzpopmax(keys, timeout); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmax_with_count_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] arguments = new String[] {key, Long.toString(count)}; + Map value = Map.of("member1", 3.0, "member2", 1.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZPopMax), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmax(key, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zpopmax_with_count_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + Map value = Map.of(gs("member1"), 3.0, gs("member2"), 1.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZPopMax), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zpopmax(key, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscore_returns_success() { + // setup + String key = "testKey"; + String member = "testMember"; + String[] arguments = new String[] {key, member}; + Double value = 3.5; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zscore(key, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscore_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + Double value = 3.5; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zscore(key, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrange_by_index_returns_success() { + // setup + String key = "testKey"; + RangeByIndex rangeByIndex = new RangeByIndex(0, 1); + String[] arguments = new String[] {key, rangeByIndex.getStart(), rangeByIndex.getEnd()}; + String[] value = new String[] {"one", "two"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrange(key, rangeByIndex); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrange_binary_by_index_returns_success() { + // setup + GlideString key = gs("testKey"); + RangeByIndex rangeByIndex = new RangeByIndex(0, 1); + GlideString[] arguments = + new GlideString[] {key, gs(rangeByIndex.getStart()), gs(rangeByIndex.getEnd())}; + GlideString[] value = new GlideString[] {gs("one"), gs("two")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrange(key, rangeByIndex); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrange_by_score_with_reverse_returns_success() { + // setup + String key = "testKey"; + RangeByScore rangeByScore = + new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); + String[] arguments = + new String[] {key, rangeByScore.getStart(), rangeByScore.getEnd(), "BYSCORE", "REV"}; + String[] value = new String[] {"two", "one"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrange(key, rangeByScore, true); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrange_binary_by_score_with_reverse_returns_success() { + // setup + GlideString key = gs("testKey"); + RangeByScore rangeByScore = + new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); + GlideString[] arguments = + new GlideString[] { + key, gs(rangeByScore.getStart()), gs(rangeByScore.getEnd()), gs("BYSCORE"), gs("REV") + }; + GlideString[] value = new GlideString[] {gs("two"), gs("one")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrange(key, rangeByScore, true); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrange_by_lex_returns_success() { + // setup + String key = "testKey"; + RangeByLex rangeByLex = + new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + String[] arguments = new String[] {key, rangeByLex.getStart(), rangeByLex.getEnd(), "BYLEX"}; + String[] value = new String[] {"a", "b"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrange(key, rangeByLex); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrange_binary_by_lex_returns_success() { + // setup + GlideString key = gs("testKey"); + RangeByLex rangeByLex = + new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + GlideString[] arguments = + new GlideString[] {key, gs(rangeByLex.getStart()), gs(rangeByLex.getEnd()), gs("BYLEX")}; + GlideString[] value = new GlideString[] {gs("a"), gs("b")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrange(key, rangeByLex); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangeWithScores_by_index_returns_success() { + // setup + String key = "testKey"; + RangeByIndex rangeByIndex = new RangeByIndex(0, 4); + String[] arguments = + new String[] {key, rangeByIndex.getStart(), rangeByIndex.getEnd(), WITH_SCORES_VALKEY_API}; + Map value = Map.of("one", 1.0, "two", 2.0, "three", 3.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zrangeWithScores(key, rangeByIndex); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangeWithScores_binary_by_index_returns_success() { + // setup + GlideString key = gs("testKey"); + RangeByIndex rangeByIndex = new RangeByIndex(0, 4); + GlideString[] arguments = + new GlideString[] { + key, gs(rangeByIndex.getStart()), gs(rangeByIndex.getEnd()), gs(WITH_SCORES_VALKEY_API) + }; + Map value = Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zrangeWithScores(key, rangeByIndex); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangeWithScores_by_score_returns_success() { + // setup + String key = "testKey"; + RangeByScore rangeByScore = + new RangeByScore( + InfScoreBound.NEGATIVE_INFINITY, + InfScoreBound.POSITIVE_INFINITY, + new RangeOptions.Limit(1, 2)); + String[] arguments = + new String[] { + key, + rangeByScore.getStart(), + rangeByScore.getEnd(), + "BYSCORE", + "LIMIT", + "1", + "2", + WITH_SCORES_VALKEY_API + }; + Map value = Map.of("two", 2.0, "three", 3.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zrangeWithScores(key, rangeByScore, false); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangeWithScores_binary_by_score_returns_success() { + // setup + GlideString key = gs("testKey"); + RangeByScore rangeByScore = + new RangeByScore( + InfScoreBound.NEGATIVE_INFINITY, + InfScoreBound.POSITIVE_INFINITY, + new RangeOptions.Limit(1, 2)); + GlideString[] arguments = + new GlideString[] { + key, + gs(rangeByScore.getStart()), + gs(rangeByScore.getEnd()), + gs("BYSCORE"), + gs("LIMIT"), + gs("1"), + gs("2"), + gs(WITH_SCORES_VALKEY_API) + }; + Map value = Map.of(gs("two"), 2.0, gs("three"), 3.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZRange), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zrangeWithScores(key, rangeByScore, false); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrank_returns_success() { + // setup + String key = "testKey"; + String member = "testMember"; + String[] arguments = new String[] {key, member}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrank(key, member); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrank_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrank(key, member); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrankWithScore_returns_success() { + // setup + String key = "testKey"; + String member = "testMember"; + String[] arguments = new String[] {key, member, WITH_SCORE_VALKEY_API}; + Object[] value = new Object[] {1, 6.0}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrankWithScore(key, member); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrevrank_returns_success() { + // setup + String key = "testKey"; + String member = "testMember"; + String[] arguments = new String[] {key, member}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRevRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrevrank(key, member); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrevrankWithScore_returns_success() { + // setup + String key = "testKey"; + String member = "testMember"; + String[] arguments = new String[] {key, member, WITH_SCORE_VALKEY_API}; + Object[] value = new Object[] {1, 6.0}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRevRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrevrankWithScore(key, member); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zmscore_returns_success() { + // setup + String key = "testKey"; + String[] members = new String[] {"member1", "member2"}; + String[] arguments = new String[] {key, "member1", "member2"}; + Double[] value = new Double[] {2.5, 8.2}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZMScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zmscore(key, members); + Double[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zmscore_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = new GlideString[] {gs("member1"), gs("member2")}; + GlideString[] arguments = new GlideString[] {key, gs("member1"), gs("member2")}; + Double[] value = new Double[] {2.5, 8.2}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZMScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zmscore(key, members); + Double[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiffstore_returns_success() { + // setup + String destKey = "testDestKey"; + String[] keys = new String[] {"testKey1", "testKey2"}; + String[] arguments = new String[] {destKey, Long.toString(keys.length), "testKey1", "testKey2"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZDiffStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zdiffstore(destKey, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiffstore_binary_returns_success() { + // setup + GlideString destKey = gs("testDestKey"); + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + GlideString[] arguments = + new GlideString[] { + destKey, gs(Long.toString(keys.length).getBytes()), gs("testKey1"), gs("testKey2") + }; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZDiffStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zdiffstore(destKey, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiff_returns_success() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {"2", key1, key2}; + String[] value = new String[] {"element1"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZDiff), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zdiff(new String[] {key1, key2}); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiff_binary_returns_success() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {gs("2"), key1, key2}; + GlideString[] value = new GlideString[] {gs("element1")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZDiff), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zdiff(new GlideString[] {key1, key2}); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiffWithScores_returns_success() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {"2", key1, key2, WITH_SCORES_VALKEY_API}; + Map value = Map.of("element1", 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZDiff), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zdiffWithScores(new String[] {key1, key2}); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zdiffWithScores_binary_returns_success() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {gs("2"), key1, key2, gs(WITH_SCORES_VALKEY_API)}; + Map value = Map.of(gs("element1"), 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZDiff), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zdiffWithScores(new GlideString[] {key1, key2}); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zcount_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key, "-inf", "10.0"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZCount), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zcount(key, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(10, true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zcount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key, gs("-inf"), gs("10.0")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZCount), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zcount(key, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(10, true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zremrangebyrank_returns_success() { + // setup + String key = "testKey"; + long start = 0; + long end = -1; + String[] arguments = new String[] {key, Long.toString(start), Long.toString(end)}; + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zremrangebyrank(key, start, end); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zremrangebyrank_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long start = 0; + long end = -1; + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }; + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zremrangebyrank(key, start, end); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zremrangebylex_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key, "-", "[z"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByLex), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zremrangebylex(key, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("z", true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zremrangebylex_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key, gs("-"), gs("[z")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByLex), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zremrangebylex(key, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("z", true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zremrangebyscore_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key, "-inf", "10.0"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zremrangebyscore(key, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(10, true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zremrangebyscore_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key, gs("-inf"), gs("10.0")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zremrangebyscore(key, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(10, true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zlexcount_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key, "-", "[c"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZLexCount), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zlexcount(key, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zlexcount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key, gs("-"), gs("[c")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZLexCount), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zlexcount(key, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", true)); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangestore_by_lex_returns_success() { + // setup + String source = "testSourceKey"; + String destination = "testDestinationKey"; + RangeByLex rangeByLex = + new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + String[] arguments = + new String[] {source, destination, rangeByLex.getStart(), rangeByLex.getEnd(), "BYLEX"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrangestore(source, destination, rangeByLex); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangestore_binary_by_lex_returns_success() { + // setup + GlideString source = gs("testSourceKey"); + GlideString destination = gs("testDestinationKey"); + RangeByLex rangeByLex = + new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + GlideString[] arguments = + new GlideString[] { + source, destination, gs(rangeByLex.getStart()), gs(rangeByLex.getEnd()), gs("BYLEX") + }; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrangestore(source, destination, rangeByLex); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangestore_by_index_returns_success() { + // setup + String source = "testSourceKey"; + String destination = "testDestinationKey"; + RangeByIndex rangeByIndex = new RangeByIndex(0, 1); + String[] arguments = + new String[] {source, destination, rangeByIndex.getStart(), rangeByIndex.getEnd()}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrangestore(source, destination, rangeByIndex); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangestore_binary_by_index_returns_success() { + // setup + GlideString source = gs("testSourceKey"); + GlideString destination = gs("testDestinationKey"); + RangeByIndex rangeByIndex = new RangeByIndex(0, 1); + GlideString[] arguments = + new GlideString[] { + source, destination, gs(rangeByIndex.getStart()), gs(rangeByIndex.getEnd()) + }; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrangestore(source, destination, rangeByIndex); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangestore_by_score_with_reverse_returns_success() { + // setup + String source = "testSourceKey"; + String destination = "testDestinationKey"; + RangeByScore rangeByScore = + new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); + boolean reversed = true; + String[] arguments = + new String[] { + source, destination, rangeByScore.getStart(), rangeByScore.getEnd(), "BYSCORE", "REV" + }; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zrangestore(source, destination, rangeByScore, reversed); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrangestore_binary_by_score_with_reverse_returns_success() { + // setup + GlideString source = gs("testSourceKey"); + GlideString destination = gs("testDestinationKey"); + RangeByScore rangeByScore = + new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); + boolean reversed = true; + GlideString[] arguments = + new GlideString[] { + source, + destination, + gs(rangeByScore.getStart()), + gs(rangeByScore.getEnd()), + gs("BYSCORE"), + gs("REV") + }; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zrangestore(source, destination, rangeByScore, reversed); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunion_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + KeyArray keyArray = new KeyArray(keys); + String[] arguments = keyArray.toArgs(); + String[] value = new String[] {"elem1", "elem2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZUnion), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zunion(keyArray); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunion_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + KeyArrayBinary keyArray = new KeyArrayBinary(keys); + GlideString[] arguments = keyArray.toArgs(); + GlideString[] value = new GlideString[] {gs("elem1"), gs("elem2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZUnion), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zunion(keyArray); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionstore_returns_success() { + // setup + String destination = "destinationKey"; + String[] keys = new String[] {"key1", "key2"}; + KeyArray keyArray = new KeyArray(keys); + String[] arguments = concatenateArrays(new String[] {destination}, keyArray.toArgs()); + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZUnionStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zunionstore(destination, keyArray); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionstore_binary_returns_success() { + // setup + GlideString destination = gs("destinationKey"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + KeyArrayBinary keyArray = new KeyArrayBinary(keys); + GlideString[] arguments = concatenateArrays(new GlideString[] {destination}, keyArray.toArgs()); + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZUnionStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zunionstore(destination, keyArray); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionstore_with_options_returns_success() { + // setup + String destination = "destinationKey"; + String[] keys = new String[] {"key1", "key2"}; + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of("key1", 10.0)); + keysWeights.add(Pair.of("key2", 20.0)); + WeightedKeys weightedKeys = new WeightedKeys(keysWeights); + Aggregate aggregate = Aggregate.MIN; + String[] arguments = + concatenateArrays(new String[] {destination}, weightedKeys.toArgs(), aggregate.toArgs()); + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZUnionStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zunionstore(destination, weightedKeys, aggregate); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionstore_with_options_binary_returns_success() { + // setup + GlideString destination = gs("destinationKey"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of(gs("key1"), 10.0)); + keysWeights.add(Pair.of(gs("key2"), 20.0)); + WeightedKeysBinary weightedKeys = new WeightedKeysBinary(keysWeights); + Aggregate aggregate = Aggregate.MIN; + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(weightedKeys.toArgs()) + .add(aggregate.toArgs()) + .toArray(); + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZUnionStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zunionstore(destination, weightedKeys, aggregate); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionWithScores_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + KeyArray keyArray = new KeyArray(keys); + String[] arguments = + concatenateArrays(keyArray.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + Map value = Map.of("elem1", 1.0, "elem2", 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZUnion), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zunionWithScores(keyArray); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionWithScores_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + KeyArrayBinary keyArray = new KeyArrayBinary(keys); + GlideString[] arguments = + concatenateArrays(keyArray.toArgs(), new GlideString[] {gs(WITH_SCORES_VALKEY_API)}); + Map value = Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZUnion), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zunionWithScores(keyArray); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionWithScores_with_options_returns_success() { + // setup + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of("key1", 10.0)); + keysWeights.add(Pair.of("key2", 20.0)); + WeightedKeys weightedKeys = new WeightedKeys(keysWeights); + Aggregate aggregate = Aggregate.MIN; + String[] arguments = + concatenateArrays( + weightedKeys.toArgs(), aggregate.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + Map value = Map.of("elem1", 1.0, "elem2", 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZUnion), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zunionWithScores(weightedKeys, aggregate); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zunionWithScores_with_options_binary_returns_success() { + // setup + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of(gs("key1"), 10.0)); + keysWeights.add(Pair.of(gs("key2"), 20.0)); + WeightedKeysBinary weightedKeys = new WeightedKeysBinary(keysWeights); + Aggregate aggregate = Aggregate.MIN; + GlideString[] arguments = + new ArgsBuilder() + .add(weightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API) + .toArray(); + Map value = Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZUnion), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zunionWithScores(weightedKeys, aggregate); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinter_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + KeyArray keyArray = new KeyArray(keys); + String[] arguments = keyArray.toArgs(); + String[] value = new String[] {"elem1", "elem2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInter), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zinter(keyArray); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinter_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + KeyArrayBinary keyArray = new KeyArrayBinary(keys); + GlideString[] arguments = keyArray.toArgs(); + GlideString[] value = new GlideString[] {gs("elem1"), gs("elem2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInter), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zinter(keyArray); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterWithScores_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + KeyArray keyArray = new KeyArray(keys); + String[] arguments = + concatenateArrays(keyArray.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + Map value = Map.of("elem1", 1.0, "elem2", 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZInter), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zinterWithScores(keyArray); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterWithScores_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + KeyArrayBinary keyArray = new KeyArrayBinary(keys); + GlideString[] arguments = + concatenateArrays(keyArray.toArgs(), new GlideString[] {gs(WITH_SCORES_VALKEY_API)}); + Map value = Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZInter), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.zinterWithScores(keyArray); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterWithScores_with_aggregation_returns_success() { + // setup + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of("key1", 10.0)); + keysWeights.add(Pair.of("key2", 20.0)); + WeightedKeys weightedKeys = new WeightedKeys(keysWeights); + Aggregate aggregate = Aggregate.MIN; + String[] arguments = + concatenateArrays( + weightedKeys.toArgs(), aggregate.toArgs(), new String[] {WITH_SCORES_VALKEY_API}); + Map value = Map.of("elem1", 1.0, "elem2", 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(ZInter), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zinterWithScores(weightedKeys, aggregate); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterWithScores_binary_with_aggregation_returns_success() { + // setup + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of(gs("key1"), 10.0)); + keysWeights.add(Pair.of(gs("key2"), 20.0)); + WeightedKeysBinary weightedKeys = new WeightedKeysBinary(keysWeights); + Aggregate aggregate = Aggregate.MIN; + GlideString[] arguments = + new ArgsBuilder() + .add(weightedKeys.toArgs()) + .add(aggregate.toArgs()) + .add(WITH_SCORES_VALKEY_API) + .toArray(); + Map value = Map.of(gs("elem1"), 1.0, gs("elem2"), 2.0); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ZInter), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.zinterWithScores(weightedKeys, aggregate); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterstore_returns_success() { + // setup + String destination = "destinationKey"; + String[] keys = new String[] {"key1", "key2"}; + KeyArray keyArray = new KeyArray(keys); + String[] arguments = concatenateArrays(new String[] {destination}, keyArray.toArgs()); + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zinterstore(destination, keyArray); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterstore_binary_returns_success() { + // setup + GlideString destination = gs("destinationKey"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + KeyArrayBinary keyArray = new KeyArrayBinary(keys); + GlideString[] arguments = concatenateArrays(new GlideString[] {destination}, keyArray.toArgs()); + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zinterstore(destination, keyArray); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterstore_with_options_returns_success() { + // setup + String destination = "destinationKey"; + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of("key1", 10.0)); + keysWeights.add(Pair.of("key2", 20.0)); + WeightedKeys weightedKeys = new WeightedKeys(keysWeights); + Aggregate aggregate = Aggregate.MIN; + String[] arguments = + concatenateArrays(new String[] {destination}, weightedKeys.toArgs(), aggregate.toArgs()); + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zinterstore(destination, weightedKeys, aggregate); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zinterstore_binary_with_options_returns_success() { + // setup + GlideString destination = gs("destinationKey"); + List> keysWeights = new ArrayList<>(); + keysWeights.add(Pair.of(gs("key1"), 10.0)); + keysWeights.add(Pair.of(gs("key2"), 20.0)); + WeightedKeysBinary weightedKeys = new WeightedKeysBinary(keysWeights); + Aggregate aggregate = Aggregate.MIN; + GlideString[] arguments = + new ArgsBuilder() + .add(destination) + .add(weightedKeys.toArgs()) + .add(aggregate.toArgs()) + .toArray(); + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zinterstore(destination, weightedKeys, aggregate); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zintercard_with_limit_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + long limit = 3L; + String[] arguments = new String[] {"2", "key1", "key2", LIMIT_VALKEY_API, "3"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zintercard_with_limit_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + long limit = 3L; + GlideString[] arguments = + new GlideString[] {gs("2"), gs("key1"), gs("key2"), gs(LIMIT_VALKEY_API), gs("3")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zintercard_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + String[] arguments = new String[] {"2", "key1", "key2"}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zintercard_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("2"), gs("key1"), gs("key2")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmember_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + String value = "testValue"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmember(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmember_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + GlideString value = gs("testValue"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmember(key); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmemberWithCount_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] arguments = new String[] {key, Long.toString(count)}; + String[] value = new String[] {"member1", "member2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmemberWithCount(key, count); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmemberWithCount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + GlideString[] value = new GlideString[] {gs("member1"), gs("member2")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmemberWithCount(key, count); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmemberWithCountWithScores_returns_success() { + // setup + String key = "testKey"; + long count = 2L; + String[] arguments = new String[] {key, Long.toString(count), WITH_SCORES_VALKEY_API}; + Object[][] value = new Object[][] {{"member1", 2.0}, {"member2", 3.0}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmemberWithCountWithScores(key, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zrandmemberWithCountWithScores_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString[] arguments = + new GlideString[] {key, gs(Long.toString(count)), gs(WITH_SCORES_VALKEY_API)}; + Object[][] value = new Object[][] {{gs("member1"), 2.0}, {gs("member2"), 3.0}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrandmemberWithCountWithScores(key, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zincrby_returns_success() { + // setup + String key = "testKey"; + double increment = 4.2; + String member = "member"; + String[] arguments = new String[] {key, "4.2", member}; + Double value = 3.14; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZIncrBy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zincrby(key, increment, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zincrby_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + double increment = 4.2; + GlideString member = gs("member"); + GlideString[] arguments = new GlideString[] {key, gs("4.2"), member}; + Double value = 3.14; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZIncrBy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zincrby(key, increment, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void xadd_returns_success() { + // setup + String key = "testKey"; + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put("testField1", "testValue1"); + fieldValues.put("testField2", "testValue2"); + String[] fieldValuesArgs = convertMapToKeyValueStringArray(fieldValues); + String[] arguments = new String[] {key, "*"}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + String returnId = "testId"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + + @SneakyThrows + @Test + public void xadd_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put(gs("testField1"), gs("testValue1")); + fieldValues.put(gs("testField2"), gs("testValue2")); + GlideString[] fieldValuesArgs = convertMapToKeyValueGlideStringArray(fieldValues); + GlideString[] arguments = new GlideString[] {key, gs("*")}; + arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); + GlideString returnId = gs("testId"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, response.get()); + } + + private static List getStreamAddOptions() { + return List.of( + Arguments.of( + // no TRIM option + "test_xadd_no_trim", + StreamAddOptions.builder().id("id").makeStream(Boolean.FALSE).build(), + new String[] {NO_MAKE_STREAM_VALKEY_API, "id"}), + Arguments.of( + // MAXLEN with LIMIT + "test_xadd_maxlen_with_limit", + StreamAddOptions.builder() + .id("id") + .makeStream(Boolean.TRUE) + .trim(new MaxLen(5L, 10L)) + .build(), + new String[] { + TRIM_MAXLEN_VALKEY_API, + TRIM_NOT_EXACT_VALKEY_API, + Long.toString(5L), + TRIM_LIMIT_VALKEY_API, + Long.toString(10L), + "id" + }), + Arguments.of( + // MAXLEN with non exact match + "test_xadd_maxlen_with_non_exact_match", + StreamAddOptions.builder() + .makeStream(Boolean.FALSE) + .trim(new MaxLen(false, 2L)) + .build(), + new String[] { + NO_MAKE_STREAM_VALKEY_API, + TRIM_MAXLEN_VALKEY_API, + TRIM_NOT_EXACT_VALKEY_API, + Long.toString(2L), + "*" + }), + Arguments.of( + // MIN ID with LIMIT + "test_xadd_minid_with_limit", + StreamAddOptions.builder() + .id("id") + .makeStream(Boolean.TRUE) + .trim(new MinId("testKey", 10L)) + .build(), + new String[] { + TRIM_MINID_VALKEY_API, + TRIM_NOT_EXACT_VALKEY_API, + "testKey", + TRIM_LIMIT_VALKEY_API, + Long.toString(10L), + "id" + }), + Arguments.of( + // MIN ID with non-exact match + "test_xadd_minid_with_non_exact_match", + StreamAddOptions.builder() + .makeStream(Boolean.FALSE) + .trim(new MinId(false, "testKey")) + .build(), + new String[] { + NO_MAKE_STREAM_VALKEY_API, + TRIM_MINID_VALKEY_API, + TRIM_NOT_EXACT_VALKEY_API, + "testKey", + "*" + })); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("getStreamAddOptions") + public void xadd_with_options_to_arguments( + String testName, StreamAddOptions options, String[] expectedArgs) { + assertArrayEquals( + expectedArgs, options.toArgs(), "Expected " + testName + " toArgs() to pass."); + } + + @SneakyThrows + @Test + public void xadd_with_nomakestream_maxlen_options_returns_success() { + // setup + String key = "testKey"; + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put("testField1", "testValue1"); + fieldValues.put("testField2", "testValue2"); + StreamAddOptions options = + StreamAddOptions.builder().id("id").makeStream(false).trim(new MaxLen(true, 5L)).build(); + + String[] arguments = + new String[] { + key, + NO_MAKE_STREAM_VALKEY_API, + TRIM_MAXLEN_VALKEY_API, + TRIM_EXACT_VALKEY_API, + Long.toString(5L), + "id" + }; + arguments = ArrayUtils.addAll(arguments, convertMapToKeyValueStringArray(fieldValues)); + + String returnId = "testId"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues, options); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, payload); + } + + @SneakyThrows + @Test + public void xadd_binary_with_nomakestream_maxlen_options_returns_success() { + // setup + GlideString key = gs("testKey"); + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put(gs("testField1"), gs("testValue1")); + fieldValues.put(gs("testField2"), gs("testValue2")); + StreamAddOptionsBinary options = + StreamAddOptionsBinary.builder() + .id(gs("id")) + .makeStream(false) + .trim(new MaxLen(true, 5L)) + .build(); + + GlideString[] arguments = + new GlideString[] { + key, + gs(NO_MAKE_STREAM_VALKEY_API), + gs(TRIM_MAXLEN_VALKEY_API), + gs(TRIM_EXACT_VALKEY_API), + gs(Long.toString(5L)), + gs("id") + }; + arguments = ArrayUtils.addAll(arguments, convertMapToKeyValueGlideStringArray(fieldValues)); + + GlideString returnId = gs("testId"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(returnId); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xadd(key, fieldValues, options); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(returnId, payload); + } + + @Test + @SneakyThrows + public void xtrim_with_exact_MinId() { + // setup + String key = "testKey"; + StreamTrimOptions limit = new MinId(true, "id"); + String[] arguments = new String[] {key, TRIM_MINID_VALKEY_API, TRIM_EXACT_VALKEY_API, "id"}; + Long completedResult = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XTrim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xtrim(key, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xtrim_binary_with_exact_MinId() { + // setup + GlideString key = gs("testKey"); + StreamTrimOptions limit = new MinId(true, "id"); + GlideString[] arguments = + new GlideString[] {key, gs(TRIM_MINID_VALKEY_API), gs(TRIM_EXACT_VALKEY_API), gs("id")}; + Long completedResult = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XTrim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xtrim(key, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + private static List getStreamTrimOptions() { + return List.of( + Arguments.of( + // MAXLEN just THRESHOLD + "test_xtrim_maxlen", new MaxLen(5L), new String[] {TRIM_MAXLEN_VALKEY_API, "5"}), + Arguments.of( + // MAXLEN with LIMIT + "test_xtrim_maxlen_with_limit", + new MaxLen(5L, 10L), + new String[] { + TRIM_MAXLEN_VALKEY_API, TRIM_NOT_EXACT_VALKEY_API, "5", TRIM_LIMIT_VALKEY_API, "10" + }), + Arguments.of( + // MAXLEN with exact + "test_xtrim_exact_maxlen", + new MaxLen(true, 10L), + new String[] {TRIM_MAXLEN_VALKEY_API, TRIM_EXACT_VALKEY_API, "10"}), + Arguments.of( + // MINID just THRESHOLD + "test_xtrim_minid", new MinId("0-1"), new String[] {TRIM_MINID_VALKEY_API, "0-1"}), + Arguments.of( + // MINID with exact + "test_xtrim_exact_minid", + new MinId(true, "0-2"), + new String[] {TRIM_MINID_VALKEY_API, TRIM_EXACT_VALKEY_API, "0-2"}), + Arguments.of( + // MINID with LIMIT + "test_xtrim_minid_with_limit", + new MinId("0-3", 10L), + new String[] { + TRIM_MINID_VALKEY_API, TRIM_NOT_EXACT_VALKEY_API, "0-3", TRIM_LIMIT_VALKEY_API, "10" + })); + } + + private static List getStreamTrimOptionsBinary() { + return List.of( + Arguments.of( + // MAXLEN just THRESHOLD + "test_xtrim_maxlen", + new MaxLen(5L), + new GlideString[] {gs(TRIM_MAXLEN_VALKEY_API), gs("5")}), + Arguments.of( + // MAXLEN with LIMIT + "test_xtrim_maxlen_with_limit", + new MaxLen(5L, 10L), + new GlideString[] { + gs(TRIM_MAXLEN_VALKEY_API), + gs(TRIM_NOT_EXACT_VALKEY_API), + gs("5"), + gs(TRIM_LIMIT_VALKEY_API), + gs("10") + }), + Arguments.of( + // MAXLEN with exact + "test_xtrim_exact_maxlen", + new MaxLen(true, 10L), + new GlideString[] {gs(TRIM_MAXLEN_VALKEY_API), gs(TRIM_EXACT_VALKEY_API), gs("10")}), + Arguments.of( + // MINID just THRESHOLD + "test_xtrim_minid", + new MinId("0-1"), + new GlideString[] {gs(TRIM_MINID_VALKEY_API), gs("0-1")}), + Arguments.of( + // MINID with exact + "test_xtrim_exact_minid", + new MinId(true, "0-2"), + new GlideString[] {gs(TRIM_MINID_VALKEY_API), gs(TRIM_EXACT_VALKEY_API), gs("0-2")}), + Arguments.of( + // MINID with LIMIT + "test_xtrim_minid_with_limit", + new MinId("0-3", 10L), + new GlideString[] { + gs(TRIM_MINID_VALKEY_API), + gs(TRIM_NOT_EXACT_VALKEY_API), + gs("0-3"), + gs(TRIM_LIMIT_VALKEY_API), + gs("10") + })); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("getStreamTrimOptions") + public void xtrim_with_options_to_arguments( + String testName, StreamTrimOptions options, String[] expectedArgs) { + assertArrayEquals(expectedArgs, options.toArgs()); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("getStreamTrimOptionsBinary") + public void xtrim_with_options_to_arguments_binary( + String testName, StreamTrimOptions options, GlideString[] expectedGlideStringArgs) { + assertArrayEquals(expectedGlideStringArgs, options.toGlideStringArgs()); + } + + @Test + @SneakyThrows + public void xlen_returns_success() { + // setup + String key = "testKey"; + Long completedResult = 99L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XLen), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xlen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xread_multiple_keys() { + // setup + String keyOne = "one"; + String streamIdOne = "id-one"; + String keyTwo = "two"; + String streamIdTwo = "id-two"; + String[][] fieldValues = {{"field", "value"}}; + Map> completedResult = new LinkedHashMap<>(); + completedResult.put(keyOne, Map.of(streamIdOne, fieldValues)); + completedResult.put(keyTwo, Map.of(streamIdTwo, fieldValues)); + String[] arguments = {READ_STREAMS_VALKEY_API, keyOne, keyTwo, streamIdOne, streamIdTwo}; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XRead), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + Map keysAndIds = new LinkedHashMap<>(); + keysAndIds.put(keyOne, streamIdOne); + keysAndIds.put(keyTwo, streamIdTwo); + CompletableFuture>> response = service.xread(keysAndIds); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xread_binary_multiple_keys() { + // setup + GlideString keyOne = gs("one"); + GlideString streamIdOne = gs("id-one"); + GlideString keyTwo = gs("two"); + GlideString streamIdTwo = gs("id-two"); + GlideString[][] fieldValues = {{gs("field"), gs("value")}}; + Map> completedResult = new LinkedHashMap<>(); + completedResult.put(keyOne, Map.of(streamIdOne, fieldValues)); + completedResult.put(keyTwo, Map.of(streamIdTwo, fieldValues)); + GlideString[] arguments = { + gs(READ_STREAMS_VALKEY_API), keyOne, keyTwo, streamIdOne, streamIdTwo + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XRead), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + Map keysAndIds = new LinkedHashMap<>(); + keysAndIds.put(keyOne, streamIdOne); + keysAndIds.put(keyTwo, streamIdTwo); + CompletableFuture>> response = + service.xreadBinary(keysAndIds); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xread_with_options() { + // setup + String keyOne = "one"; + String streamIdOne = "id-one"; + Long block = 2L; + Long count = 10L; + String[][] fieldValues = {{"field", "value"}}; + Map> completedResult = + Map.of(keyOne, Map.of(streamIdOne, fieldValues)); + String[] arguments = { + READ_COUNT_VALKEY_API, + count.toString(), + READ_BLOCK_VALKEY_API, + block.toString(), + READ_STREAMS_VALKEY_API, + keyOne, + streamIdOne + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XRead), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>> response = + service.xread( + Map.of(keyOne, streamIdOne), + StreamReadOptions.builder().block(block).count(count).build()); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xread_with_options_binary() { + // setup + GlideString keyOne = gs("one"); + GlideString streamIdOne = gs("id-one"); + Long block = 2L; + Long count = 10L; + GlideString[][] fieldValues = {{gs("field"), gs("value")}}; + Map> completedResult = + Map.of(keyOne, Map.of(streamIdOne, fieldValues)); + GlideString[] arguments = { + gs(READ_COUNT_VALKEY_API), + gs(count.toString()), + gs(READ_BLOCK_VALKEY_API), + gs(block.toString()), + gs(READ_STREAMS_VALKEY_API), + keyOne, + streamIdOne + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XRead), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>> response = + service.xreadBinary( + Map.of(keyOne, streamIdOne), + StreamReadOptions.builder().block(block).count(count).build()); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xdel_returns_success() { + // setup + String key = "testKey"; + String[] ids = {"one-1", "two-2", "three-3"}; + Long completedResult = 69L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(XDel), eq(new String[] {key, "one-1", "two-2", "three-3"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xdel(key, ids); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xdel_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] ids = {gs("one-1"), gs("two-2"), gs("three-3")}; + Long completedResult = 69L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(XDel), eq(new GlideString[] {key, gs("one-1"), gs("two-2"), gs("three-3")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xdel(key, ids); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrange_returns_success() { + // setup + String key = "testKey"; + StreamRange start = IdBound.of(9999L); + StreamRange end = IdBound.ofExclusive("696969-10"); + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRange), eq(new String[] {key, "9999", "(696969-10"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrange(key, start, end); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange start = IdBound.of(9999L); + StreamRange end = IdBound.ofExclusive("696969-10"); + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRange), eq(new GlideString[] {key, gs("9999"), gs("(696969-10")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrange(key, start, end); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrange_withcount_returns_success() { + // setup + String key = "testKey"; + StreamRange start = InfRangeBound.MIN; + StreamRange end = InfRangeBound.MAX; + long count = 99L; + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRange), + eq( + new String[] { + key, + MINIMUM_RANGE_VALKEY_API, + MAXIMUM_RANGE_VALKEY_API, + RANGE_COUNT_VALKEY_API, + Long.toString(count) + }), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrange(key, start, end, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrange_binary_withcount_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange start = InfRangeBound.MIN; + StreamRange end = InfRangeBound.MAX; + long count = 99L; + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRange), + eq( + new GlideString[] { + key, + gs(MINIMUM_RANGE_VALKEY_API), + gs(MAXIMUM_RANGE_VALKEY_API), + gs(RANGE_COUNT_VALKEY_API), + gs(Long.toString(count)) + }), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xrange(key, start, end, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrevrange_returns_success() { + // setup + String key = "testKey"; + StreamRange end = IdBound.of(9999L); + StreamRange start = IdBound.ofExclusive("696969-10"); + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), eq(new String[] {key, "9999", "(696969-10"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrevrange(key, end, start); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrevrange_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange end = IdBound.of(9999L); + StreamRange start = IdBound.ofExclusive("696969-10"); + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), eq(new GlideString[] {key, gs("9999"), gs("(696969-10")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xrevrange(key, end, start); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrevrange_withcount_returns_success() { + // setup + String key = "testKey"; + StreamRange end = InfRangeBound.MAX; + StreamRange start = InfRangeBound.MIN; + long count = 99L; + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), + eq( + new String[] { + key, + MAXIMUM_RANGE_VALKEY_API, + MINIMUM_RANGE_VALKEY_API, + RANGE_COUNT_VALKEY_API, + Long.toString(count) + }), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xrevrange(key, end, start, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @Test + @SneakyThrows + public void xrevrange_binary_withcount_returns_success() { + // setup + GlideString key = gs("testKey"); + StreamRange end = InfRangeBound.MAX; + StreamRange start = InfRangeBound.MIN; + long count = 99L; + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XRevRange), + eq( + new GlideString[] { + key, + gs(MAXIMUM_RANGE_VALKEY_API), + gs(MINIMUM_RANGE_VALKEY_API), + gs(RANGE_COUNT_VALKEY_API), + gs(Long.toString(count)) + }), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xrevrange(key, end, start, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xgroupCreate() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String id = "testId"; + String[] arguments = new String[] {key, groupName, id}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupCreate), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupCreate(key, groupName, id); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupCreate_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString id = gs("testId"); + GlideString[] arguments = new GlideString[] {key, groupName, id}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupCreate), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupCreate(key, groupName, id); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupCreate_withOptions() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String id = "testId"; + Long testEntry = 123L; + StreamGroupOptions options = + StreamGroupOptions.builder().makeStream().entriesRead(testEntry).build(); + String[] arguments = + new String[] { + key, + groupName, + id, + MAKE_STREAM_VALKEY_API, + ENTRIES_READ_VALKEY_API, + Long.toString(testEntry) + }; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupCreate), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupCreate(key, groupName, id, options); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupCreate_withOptions_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString id = gs("testId"); + Long testEntry = 123L; + StreamGroupOptions options = + StreamGroupOptions.builder().makeStream().entriesRead(testEntry).build(); + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(groupName) + .add(id) + .add(MAKE_STREAM_VALKEY_API) + .add(ENTRIES_READ_VALKEY_API) + .add(testEntry) + .toArray(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupCreate), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupCreate(key, groupName, id, options); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupDestroy() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String[] arguments = new String[] {key, groupName}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupDestroy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupDestroy(key, groupName); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(Boolean.TRUE, payload); + } + + @SneakyThrows + @Test + public void xgroupDestroy_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString[] arguments = new GlideString[] {key, groupName}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupDestroy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupDestroy(key, groupName); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(Boolean.TRUE, payload); + } + + @SneakyThrows + @Test + public void xgroupCreateConsumer() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumerName = "testConsumerName"; + String[] arguments = new String[] {key, groupName, consumerName}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupCreateConsumer), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xgroupCreateConsumer(key, groupName, consumerName); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(Boolean.TRUE, payload); + } + + @SneakyThrows + @Test + public void xgroupCreateConsumer_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumerName = gs("testConsumerName"); + GlideString[] arguments = new GlideString[] {key, groupName, consumerName}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(Boolean.TRUE); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupCreateConsumer), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xgroupCreateConsumer(key, groupName, consumerName); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(Boolean.TRUE, payload); + } + + @SneakyThrows + @Test + public void xgroupDelConsumer() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumerName = "testConsumerName"; + String[] arguments = new String[] {key, groupName, consumerName}; + Long result = 28L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupDelConsumer), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupDelConsumer(key, groupName, consumerName); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void xgroupDelConsumer_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumerName = gs("testConsumerName"); + GlideString[] arguments = new GlideString[] {key, groupName, consumerName}; + Long result = 28L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupDelConsumer), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupDelConsumer(key, groupName, consumerName); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void xgroupSetid() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String id = "testId"; + String[] arguments = new String[] {key, groupName, id}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupSetId), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupSetId(key, groupName, id); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupSetid_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString id = gs("testId"); + GlideString[] arguments = new GlideString[] {key, groupName, id}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupSetId), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupSetId(key, groupName, id); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupSetidWithEntriesRead() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String id = "testId"; + Long entriesRead = 1L; + String[] arguments = + new String[] {key, groupName, id, ENTRIES_READ_VALKEY_API, Long.toString(entriesRead)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupSetId), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupSetId(key, groupName, id, entriesRead); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xgroupSetidWithEntriesRead_binary() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString id = gs("testId"); + Long entriesRead = 1L; + GlideString[] arguments = + new ArgsBuilder() + .add(key) + .add(groupName) + .add(id) + .add(ENTRIES_READ_VALKEY_API) + .add(entriesRead) + .toArray(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XGroupSetId), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xgroupSetId(key, groupName, id, entriesRead); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void xreadgroup_multiple_keys() { + // setup + String keyOne = "one"; + String streamIdOne = "id-one"; + String keyTwo = "two"; + String streamIdTwo = "id-two"; + String groupName = "testGroup"; + String consumerName = "consumerGroup"; + String[][] fieldValues = {{"field", "value"}}; + Map> completedResult = new LinkedHashMap<>(); + completedResult.put(keyOne, Map.of(streamIdOne, fieldValues)); + completedResult.put(keyTwo, Map.of(streamIdTwo, fieldValues)); + String[] arguments = { + READ_GROUP_VALKEY_API, + groupName, + consumerName, + READ_STREAMS_VALKEY_API, + keyOne, + keyTwo, + streamIdOne, + streamIdTwo + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XReadGroup), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + Map keysAndIds = new LinkedHashMap<>(); + keysAndIds.put(keyOne, streamIdOne); + keysAndIds.put(keyTwo, streamIdTwo); + CompletableFuture>> response = + service.xreadgroup(keysAndIds, groupName, consumerName); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xreadgroup_binary_multiple_keys() { + // setup + GlideString keyOne = gs("one"); + GlideString streamIdOne = gs("id-one"); + GlideString keyTwo = gs("two"); + GlideString streamIdTwo = gs("id-two"); + GlideString groupName = gs("testGroup"); + GlideString consumerName = gs("consumerGroup"); + GlideString[][] fieldValues = {{gs("field"), gs("value")}}; + Map> completedResult = new LinkedHashMap<>(); + completedResult.put(keyOne, Map.of(streamIdOne, fieldValues)); + completedResult.put(keyTwo, Map.of(streamIdTwo, fieldValues)); + GlideString[] arguments = { + gs(READ_GROUP_VALKEY_API), + groupName, + consumerName, + gs(READ_STREAMS_VALKEY_API), + keyOne, + keyTwo, + streamIdOne, + streamIdTwo + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XReadGroup), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + Map keysAndIds = new LinkedHashMap<>(); + keysAndIds.put(keyOne, streamIdOne); + keysAndIds.put(keyTwo, streamIdTwo); + CompletableFuture>> response = + service.xreadgroup(keysAndIds, groupName, consumerName); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xreadgroup_with_options() { + // setup + String keyOne = "one"; + String streamIdOne = "id-one"; + Long block = 2L; + Long count = 10L; + String groupName = "testGroup"; + String consumerName = "consumerGroup"; + String[][] fieldValues = {{"field", "value"}}; + Map> completedResult = + Map.of(keyOne, Map.of(streamIdOne, fieldValues)); + String[] arguments = { + READ_GROUP_VALKEY_API, + groupName, + consumerName, + READ_COUNT_VALKEY_API, + count.toString(), + READ_BLOCK_VALKEY_API, + block.toString(), + READ_NOACK_VALKEY_API, + READ_STREAMS_VALKEY_API, + keyOne, + streamIdOne + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XReadGroup), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>> response = + service.xreadgroup( + Map.of(keyOne, streamIdOne), + groupName, + consumerName, + StreamReadGroupOptions.builder().block(block).count(count).noack().build()); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xreadgroup_with_options_binary() { + // setup + GlideString keyOne = gs("one"); + GlideString streamIdOne = gs("id-one"); + Long block = 2L; + Long count = 10L; + GlideString groupName = gs("testGroup"); + GlideString consumerName = gs("consumerGroup"); + GlideString[][] fieldValues = {{gs("field"), gs("value")}}; + Map> completedResult = + Map.of(keyOne, Map.of(streamIdOne, fieldValues)); + GlideString[] arguments = { + gs(READ_GROUP_VALKEY_API), + groupName, + consumerName, + gs(READ_COUNT_VALKEY_API), + gs(count.toString()), + gs(READ_BLOCK_VALKEY_API), + gs(block.toString()), + gs(READ_NOACK_VALKEY_API), + gs(READ_STREAMS_VALKEY_API), + keyOne, + streamIdOne + }; + + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(XReadGroup), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>> response = + service.xreadgroup( + Map.of(keyOne, streamIdOne), + groupName, + consumerName, + StreamReadGroupOptions.builder().block(block).count(count).noack().build()); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + + @SneakyThrows + @Test + public void xack_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String[] ids = new String[] {"testId"}; + String[] arguments = concatenateArrays(new String[] {key, groupName}, ids); + Long mockResult = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAck), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xack(key, groupName, ids); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaim_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String[] ids = new String[] {"testId"}; + String[] arguments = concatenateArrays(new String[] {key, groupName, consumer, "18"}, ids); + Map mockResult = Map.of("1234-0", new String[][] {{"message", "log"}}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xclaim(key, groupName, consumer, minIdleTime, ids); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaim_biary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString[] ids = new GlideString[] {gs("testId")}; + GlideString[] arguments = + concatenateArrays(new GlideString[] {key, groupName, consumer, gs("18")}, ids); + Map mockResult = + Map.of(gs("1234-0"), new GlideString[][] {{gs("message"), gs("log")}}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xclaim(key, groupName, consumer, minIdleTime, ids); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaim_with_options_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String[] ids = new String[] {"testId"}; + StreamClaimOptions options = + StreamClaimOptions.builder().force().idle(11L).idleUnixTime(12L).retryCount(5L).build(); + String[] arguments = + new String[] { + key, + groupName, + consumer, + "18", + "testId", + IDLE_VALKEY_API, + "11", + TIME_VALKEY_API, + "12", + RETRY_COUNT_VALKEY_API, + "5", + FORCE_VALKEY_API + }; + Map mockResult = Map.of("1234-0", new String[][] {{"message", "log"}}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xclaim(key, groupName, consumer, minIdleTime, ids, options); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaim_binary_with_options_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString[] ids = new GlideString[] {gs("testId")}; + StreamClaimOptions options = + StreamClaimOptions.builder().force().idle(11L).idleUnixTime(12L).retryCount(5L).build(); + GlideString[] arguments = + new GlideString[] { + key, + groupName, + consumer, + gs("18"), + gs("testId"), + gs(IDLE_VALKEY_API), + gs("11"), + gs(TIME_VALKEY_API), + gs("12"), + gs(RETRY_COUNT_VALKEY_API), + gs("5"), + gs(FORCE_VALKEY_API) + }; + Map mockResult = + Map.of(gs("1234-0"), new GlideString[][] {{gs("message"), gs("log")}}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.xclaim(key, groupName, consumer, minIdleTime, ids, options); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaimJustId_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String[] ids = new String[] {"testId"}; + String[] arguments = + new String[] {key, groupName, consumer, "18", "testId", JUST_ID_VALKEY_API}; + String[] mockResult = {"message", "log"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xclaimJustId(key, groupName, consumer, minIdleTime, ids); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaimJustId_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString[] ids = new GlideString[] {gs("testId")}; + GlideString[] arguments = + new GlideString[] { + key, groupName, consumer, gs("18"), gs("testId"), gs(JUST_ID_VALKEY_API) + }; + GlideString[] mockResult = {gs("message"), gs("log")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xclaimJustId(key, groupName, consumer, minIdleTime, ids); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaimJustId_with_options_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String[] ids = new String[] {"testId"}; + StreamClaimOptions options = + StreamClaimOptions.builder().force().idle(11L).idleUnixTime(12L).retryCount(5L).build(); + String[] arguments = + new String[] { + key, + groupName, + consumer, + "18", + "testId", + IDLE_VALKEY_API, + "11", + TIME_VALKEY_API, + "12", + RETRY_COUNT_VALKEY_API, + "5", + FORCE_VALKEY_API, + JUST_ID_VALKEY_API + }; + String[] mockResult = {"message", "log"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xclaimJustId(key, groupName, consumer, minIdleTime, ids, options); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xclaimJustId_binary_with_options_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString[] ids = new GlideString[] {gs("testId")}; + StreamClaimOptions options = + StreamClaimOptions.builder().force().idle(11L).idleUnixTime(12L).retryCount(5L).build(); + GlideString[] arguments = + new GlideString[] { + key, + groupName, + consumer, + gs("18"), + gs("testId"), + gs(IDLE_VALKEY_API), + gs("11"), + gs(TIME_VALKEY_API), + gs("12"), + gs(RETRY_COUNT_VALKEY_API), + gs("5"), + gs(FORCE_VALKEY_API), + gs(JUST_ID_VALKEY_API) + }; + GlideString[] mockResult = {gs("message"), gs("log")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xclaimJustId(key, groupName, consumer, minIdleTime, ids, options); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaim_return_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String start = "0-0"; + + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + String[] deletedMessageIds = new String[] {"13-1", "46-2", "89-3"}; + + String[] arguments = concatenateArrays(new String[] {key, groupName, consumer, "18", start}); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaim(key, groupName, consumer, minIdleTime, start); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaim_binary_return_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString start = gs("0-0"); + + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + GlideString[] deletedMessageIds = new GlideString[] {gs("13-1"), gs("46-2"), gs("89-3")}; + + GlideString[] arguments = + concatenateArrays(new GlideString[] {key, groupName, consumer, gs("18"), start}); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaim(key, groupName, consumer, minIdleTime, start); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaim_with_count_return_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String start = "0-0"; + long count = 1234; + + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + String[] deletedMessageIds = new String[] {"13-1", "46-2", "89-3"}; + + String[] arguments = + concatenateArrays( + new String[] {key, groupName, consumer, "18", start, "COUNT", Long.toString(count)}); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaim(key, groupName, consumer, minIdleTime, start, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaim_binary_with_count_return_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString start = gs("0-0"); + long count = 1234; + + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + GlideString[] deletedMessageIds = new GlideString[] {gs("13-1"), gs("46-2"), gs("89-3")}; + + GlideString[] arguments = + concatenateArrays( + new GlideString[] { + key, groupName, consumer, gs("18"), start, gs("COUNT"), gs(Long.toString(count)) + }); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaim(key, groupName, consumer, minIdleTime, start, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaimJustId_return_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String start = "0-0"; + + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + String[] deletedMessageIds = new String[] {"13-1", "46-2", "89-3"}; + + String[] arguments = + concatenateArrays(new String[] {key, groupName, consumer, "18", start, "JUSTID"}); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaimJustId(key, groupName, consumer, minIdleTime, start); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaimJustId_binary_return_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString start = gs("0-0"); + + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + GlideString[] deletedMessageIds = new GlideString[] {gs("13-1"), gs("46-2"), gs("89-3")}; + + GlideString[] arguments = + concatenateArrays( + new GlideString[] {key, groupName, consumer, gs("18"), start, gs("JUSTID")}); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaimJustId(key, groupName, consumer, minIdleTime, start); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaimJustId_with_count_return_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + Long minIdleTime = 18L; + String start = "0-0"; + long count = 1234; + + String[][] fieldValuesResult = {{"duration", "12345"}, {"event-id", "2"}, {"user-id", "42"}}; + Map completedResult = Map.of(key, fieldValuesResult); + + String[] deletedMessageIds = new String[] {"13-1", "46-2", "89-3"}; + + String[] arguments = + concatenateArrays( + new String[] { + key, + groupName, + consumer, + Long.toString(minIdleTime), + start, + "COUNT", + Long.toString(count), + "JUSTID" + }); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaimJustId(key, groupName, consumer, minIdleTime, start, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xautoclaimJustId_binary_with_count_return_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + Long minIdleTime = 18L; + GlideString start = gs("0-0"); + long count = 1234; + + GlideString[][] fieldValuesResult = { + {gs("duration"), gs("12345")}, {gs("event-id"), gs("2")}, {gs("user-id"), gs("42")} + }; + Map completedResult = Map.of(key, fieldValuesResult); + + GlideString[] deletedMessageIds = new GlideString[] {gs("13-1"), gs("46-2"), gs("89-3")}; + + GlideString[] arguments = + concatenateArrays( + new GlideString[] { + key, + groupName, + consumer, + gs(Long.toString(minIdleTime)), + start, + gs("COUNT"), + gs(Long.toString(count)), + gs("JUSTID") + }); + Object[] mockResult = new Object[] {start, completedResult, deletedMessageIds}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAutoClaim), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xautoclaimJustId(key, groupName, consumer, minIdleTime, start, count); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xack_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString[] ids = new GlideString[] {gs("testId")}; + GlideString[] arguments = concatenateArrays(new GlideString[] {key, groupName}, ids); + Long mockResult = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XAck), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xack(key, groupName, ids); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xpending_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String[] arguments = {key, groupName}; + Object[] summary = new Object[] {1L, "1234-0", "2345-4", new Object[][] {{"consumer", "4"}}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XPending), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xpending(key, groupName); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xpending_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString[] arguments = {key, groupName}; + Object[] summary = new Object[] {1L, "1234-0", "2345-4", new Object[][] {{"consumer", "4"}}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XPending), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xpending(key, groupName); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xpending_with_start_end_count_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String[] arguments = {key, groupName, EXCLUSIVE_RANGE_VALKEY_API + "1234-0", "2345-5", "4"}; + StreamRange start = IdBound.ofExclusive("1234-0"); + StreamRange end = IdBound.of("2345-5"); + Long count = 4L; + Object[][] extendedForm = new Object[][] {{"1234-0", "consumer", 4L, 1L}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(extendedForm); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XPending), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xpending(key, groupName, start, end, count); + Object[][] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(extendedForm, payload); + } + + @SneakyThrows + @Test + public void xpending_binary_with_start_end_count_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString[] arguments = { + key, groupName, gs(EXCLUSIVE_RANGE_VALKEY_API + "1234-0"), gs("2345-5"), gs("4") + }; + StreamRange start = IdBound.ofExclusive("1234-0"); + StreamRange end = IdBound.of("2345-5"); + Long count = 4L; + Object[][] extendedForm = new Object[][] {{"1234-0", "consumer", 4L, 1L}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(extendedForm); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XPending), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xpending(key, groupName, start, end, count); + Object[][] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(extendedForm, payload); + } + + @SneakyThrows + @Test + public void xpending_with_start_end_count_options_returns_success() { + // setup + String key = "testKey"; + String groupName = "testGroupName"; + String consumer = "testConsumer"; + String[] arguments = { + key, + groupName, + IDLE_TIME_VALKEY_API, + "100", + MINIMUM_RANGE_VALKEY_API, + MAXIMUM_RANGE_VALKEY_API, + "4", + consumer + }; + StreamRange start = InfRangeBound.MIN; + StreamRange end = InfRangeBound.MAX; + Long count = 4L; + Object[][] extendedForm = new Object[][] {{"1234-0", consumer, 4L, 1L}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(extendedForm); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XPending), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xpending( + key, + groupName, + start, + end, + count, + StreamPendingOptions.builder().minIdleTime(100L).consumer(consumer).build()); + Object[][] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(extendedForm, payload); + } + + @SneakyThrows + @Test + public void xpending_binary_with_start_end_count_options_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("testGroupName"); + GlideString consumer = gs("testConsumer"); + GlideString[] arguments = { + key, + groupName, + gs(IDLE_TIME_VALKEY_API), + gs("100"), + gs(MINIMUM_RANGE_VALKEY_API), + gs(MAXIMUM_RANGE_VALKEY_API), + gs("4"), + consumer + }; + StreamRange start = InfRangeBound.MIN; + StreamRange end = InfRangeBound.MAX; + Long count = 4L; + Object[][] extendedForm = new Object[][] {{"1234-0", consumer, 4L, 1L}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(extendedForm); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(XPending), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.xpending( + key, + groupName, + start, + end, + count, + StreamPendingOptionsBinary.builder().minIdleTime(100L).consumer(consumer).build()); + Object[][] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(extendedForm, payload); + } + + @SneakyThrows + @Test + public void type_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + String value = "none"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Type), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.type(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void type_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + String value = "none"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Type), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.type(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void randomKey() { + // setup + String key1 = "key1"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(key1); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RandomKey), eq(new String[0]), any())) + .thenReturn(testResponse); + CompletableFuture response = service.randomKey(); + + // verify + assertEquals(testResponse, response); + } + + @SneakyThrows + @Test + public void randomKeyBinary() { + // setup + GlideString key1 = gs("key1"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(key1); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RandomKey), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + CompletableFuture response = service.randomKeyBinary(); + + // verify + assertEquals(testResponse, response); + } + + @SneakyThrows + @Test + public void rename() { + // setup + String key = "key1"; + String newKey = "key2"; + String[] arguments = new String[] {key, newKey}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Rename), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rename(key, newKey); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void rename_binary() { + // setup + GlideString key = gs("key1"); + GlideString newKey = gs("key2"); + GlideString[] arguments = new GlideString[] {key, newKey}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Rename), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rename(key, newKey); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void renamenx_returns_success() { + // setup + String key = "key1"; + String newKey = "key2"; + String[] arguments = new String[] {key, newKey}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RenameNX), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.renamenx(key, newKey); + + // verify + assertEquals(testResponse, response); + assertTrue(response.get()); + } + + @SneakyThrows + @Test + public void renamenx_binary_returns_success() { + // setup + GlideString key = gs("key1"); + GlideString newKey = gs("key2"); + GlideString[] arguments = new GlideString[] {key, newKey}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RenameNX), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.renamenx(key, newKey); + + // verify + assertEquals(testResponse, response); + assertTrue(response.get()); + } + + @SneakyThrows + @Test + public void time_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + String[] payload = new String[] {"UnixTime", "ms"}; + testResponse.complete(payload); + // match on protobuf request + when(commandManager.submitNewCommand(eq(Time), eq(new String[0]), any())) + .thenReturn(testResponse); + // exercise + CompletableFuture response = service.time(); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void lastsave_returns_success() { + // setup + Long value = 42L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LastSave), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lastsave(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void flushall_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FlushAll), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushall(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushall_with_mode_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushAll), eq(new String[] {SYNC.toString()}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushall(SYNC); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushdb_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FlushDB), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushdb(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushdb_with_mode_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushDB), eq(new String[] {SYNC.toString()}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushdb(SYNC); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void lolwut_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Lolwut), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {"1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Lolwut), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Lolwut), eq(new String[] {VERSION_VALKEY_API, "42"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {VERSION_VALKEY_API, "42", "1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Lolwut), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42, params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void dbsize_returns_success() { + // setup + Long value = 10L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(DBSize), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.dbsize(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void linsert_returns_success() { + // setup + String key = "testKey"; + var position = BEFORE; + String pivot = "pivot"; + String elem = "elem"; + String[] arguments = new String[] {key, position.toString(), pivot, elem}; + long value = 42; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LInsert), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.linsert(key, position, pivot, elem); + long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void linsert_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + var position = BEFORE; + GlideString pivot = gs("pivot"); + GlideString elem = gs("elem"); + GlideString[] arguments = new GlideString[] {key, gs(position.toString()), pivot, elem}; + long value = 42; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LInsert), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.linsert(key, position, pivot, elem); + long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blpop_returns_success() { + // setup + String key = "key"; + double timeout = 0.5; + String[] arguments = new String[] {key, "0.5"}; + String[] value = new String[] {"key", "value"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BLPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.blpop(new String[] {key}, timeout); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blpop_binary_returns_success() { + // setup + GlideString key = gs("key"); + double timeout = 0.5; + GlideString[] arguments = new GlideString[] {key, gs("0.5")}; + GlideString[] value = new GlideString[] {gs("key"), gs("value")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BLPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.blpop(new GlideString[] {key}, timeout); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpushx_returns_success() { + // setup + String key = "testKey"; + String[] elements = new String[] {"value1", "value2"}; + String[] args = new String[] {key, "value1", "value2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void rpushx_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpushx_returns_success() { + // setup + String key = "testKey"; + String[] elements = new String[] {"value1", "value2"}; + String[] args = new String[] {key, "value1", "value2"}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lpushx_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void brpop_returns_success() { + // setup + String key = "key"; + double timeout = 0.5; + String[] arguments = new String[] {key, "0.5"}; + String[] value = new String[] {"key", "value"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BRPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.brpop(new String[] {key}, timeout); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void brpop_binary_returns_success() { + // setup + GlideString key = gs("key"); + double timeout = 0.5; + GlideString[] arguments = new GlideString[] {key, gs("0.5")}; + GlideString[] value = new GlideString[] {gs("key"), gs("value")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BRPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.brpop(new GlideString[] {key}, timeout); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pfadd_returns_success() { + // setup + String key = "testKey"; + String[] elements = new String[] {"a", "b", "c"}; + String[] arguments = new String[] {key, "a", "b", "c"}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PfAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pfadd(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pfadd_returns_success_binary() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("a"), gs("b"), gs("c")}; + GlideString[] arguments = new GlideString[] {key, gs("a"), gs("b"), gs("c")}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PfAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pfadd(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void pfcount_returns_success() { + // setup + String[] keys = new String[] {"a", "b", "c"}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PfCount), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pfcount(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void pfcount_returns_success_binary() { + // setup + GlideString[] keys = new GlideString[] {gs("a"), gs("b"), gs("c")}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PfCount), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pfcount(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void pfmerge_returns_success() { + // setup + String destKey = "testKey"; + String[] sourceKeys = new String[] {"a", "b", "c"}; + String[] arguments = new String[] {destKey, "a", "b", "c"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PfMerge), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pfmerge(destKey, sourceKeys); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void pfmerge_returns_success_binary() { + // setup + GlideString destKey = gs("testKey"); + GlideString[] sourceKeys = new GlideString[] {gs("a"), gs("b"), gs("c")}; + GlideString[] arguments = new GlideString[] {destKey, gs("a"), gs("b"), gs("c")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PfMerge), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pfmerge(destKey, sourceKeys); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void objectEncoding_returns_success() { + // setup + String key = "testKey"; + String encoding = "testEncoding"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(encoding); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ObjectEncoding), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectEncoding(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(encoding, payload); + } + + @SneakyThrows + @Test + public void objectEncoding_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + String encoding = "testEncoding"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(encoding); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ObjectEncoding), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectEncoding(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(encoding, payload); + } + + @SneakyThrows + @Test + public void objectFreq_returns_success() { + // setup + String key = "testKey"; + Long frequency = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(frequency); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ObjectFreq), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectFreq(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(frequency, payload); + } + + @SneakyThrows + @Test + public void objectFreq_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long frequency = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(frequency); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ObjectFreq), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectFreq(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(frequency, payload); + } + + @SneakyThrows + @Test + public void objectIdletime_returns_success() { + // setup + String key = "testKey"; + Long idletime = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(idletime); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ObjectIdleTime), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectIdletime(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(idletime, payload); + } + + @SneakyThrows + @Test + public void objectIdletime_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long idletime = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(idletime); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ObjectIdleTime), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectIdletime(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(idletime, payload); + } + + @SneakyThrows + @Test + public void objectRefcount_returns_success() { + // setup + String key = "testKey"; + Long refcount = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(refcount); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ObjectRefCount), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectRefcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(refcount, payload); + } + + @SneakyThrows + @Test + public void objectRefcount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long refcount = 0L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(refcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ObjectRefCount), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.objectRefcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(refcount, payload); + } + + @SneakyThrows + @Test + public void touch_returns_success() { + // setup + String[] keys = new String[] {"testKey1", "testKey2"}; + Long value = 2L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Touch), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.touch(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void touch_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + Long value = 2L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Touch), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.touch(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geoadd_returns_success() { + // setup + String key = "testKey"; + Map membersToGeoSpatialData = new LinkedHashMap<>(); + membersToGeoSpatialData.put("Catania", new GeospatialData(15.087269, 40)); + membersToGeoSpatialData.put("Palermo", new GeospatialData(13.361389, 38.115556)); + String[] arguments = + new String[] {key, "15.087269", "40.0", "Catania", "13.361389", "38.115556", "Palermo"}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geoadd(key, membersToGeoSpatialData); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geoadd_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Map membersToGeoSpatialData = new LinkedHashMap<>(); + membersToGeoSpatialData.put(gs("Catania"), new GeospatialData(15.087269, 40)); + membersToGeoSpatialData.put(gs("Palermo"), new GeospatialData(13.361389, 38.115556)); + GlideString[] arguments = + new GlideString[] { + key, + gs("15.087269"), + gs("40.0"), + gs("Catania"), + gs("13.361389"), + gs("38.115556"), + gs("Palermo") + }; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geoadd(key, membersToGeoSpatialData); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geoadd_with_options_returns_success() { + // setup + String key = "testKey"; + Map membersToGeoSpatialData = new LinkedHashMap<>(); + membersToGeoSpatialData.put("Catania", new GeospatialData(15.087269, 40)); + membersToGeoSpatialData.put("Palermo", new GeospatialData(13.361389, 38.115556)); + GeoAddOptions options = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS, true); + String[] arguments = + new String[] { + key, + ConditionalChange.ONLY_IF_EXISTS.getValkeyApi(), + CHANGED_VALKEY_API, + "15.087269", + "40.0", + "Catania", + "13.361389", + "38.115556", + "Palermo" + }; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geoadd(key, membersToGeoSpatialData, options); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geoadd_with_options_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Map membersToGeoSpatialData = new LinkedHashMap<>(); + membersToGeoSpatialData.put(gs("Catania"), new GeospatialData(15.087269, 40)); + membersToGeoSpatialData.put(gs("Palermo"), new GeospatialData(13.361389, 38.115556)); + GeoAddOptions options = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS, true); + GlideString[] arguments = + new GlideString[] { + key, + gs(ConditionalChange.ONLY_IF_EXISTS.getValkeyApi()), + gs(CHANGED_VALKEY_API), + gs("15.087269"), + gs("40.0"), + gs("Catania"), + gs("13.361389"), + gs("38.115556"), + gs("Palermo") + }; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoAdd), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geoadd(key, membersToGeoSpatialData, options); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geopos_returns_success() { + // setup + String key = "testKey"; + String[] members = {"Catania", "Palermo"}; + String[] arguments = new String[] {key, "Catania", "Palermo"}; + Double[][] value = {{15.087269, 40.0}, {13.361389, 38.115556}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geopos(key, members); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geopos_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = {gs("Catania"), gs("Palermo")}; + GlideString[] arguments = new GlideString[] {key, gs("Catania"), gs("Palermo")}; + Double[][] value = {{15.087269, 40.0}, {13.361389, 38.115556}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geopos(key, members); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void append() { + // setup + String key = "testKey"; + String value = "testValue"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(1L); + when(commandManager.submitNewCommand(eq(Append), eq(new String[] {key, value}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.append(key, value); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(1L, payload); + } + + @SneakyThrows + @Test + public void geohash_returns_success() { + // setup + String key = "testKey"; + String[] members = {"Catania", "Palermo", "NonExisting"}; + String[] arguments = new String[] {key, "Catania", "Palermo", "NonExisting"}; + String[] value = {"sqc8b49rny0", "sqdtr74hyu0", null}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoHash), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geohash(key, members); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geohash_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = {gs("Catania"), gs("Palermo"), gs("NonExisting")}; + GlideString[] arguments = + new GlideString[] {key, gs("Catania"), gs("Palermo"), gs("NonExisting")}; + GlideString[] value = {gs("sqc8b49rny0"), gs("sqdtr74hyu0"), null}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoHash), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geohash(key, members); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geodist_returns_success() { + // setup + String key = "testKey"; + String member1 = "Catania"; + String member2 = "Palermo"; + String[] arguments = new String[] {key, member1, member2}; + Double value = 166274.1516; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoDist), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geodist(key, member1, member2); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geodist_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member1 = gs("Catania"); + GlideString member2 = gs("Palermo"); + GlideString[] arguments = new GlideString[] {key, member1, member2}; + Double value = 166274.1516; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoDist), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geodist(key, member1, member2); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geodist_with_metrics_returns_success() { + // setup + String key = "testKey"; + String member1 = "Catania"; + String member2 = "Palermo"; + GeoUnit geoUnit = GeoUnit.KILOMETERS; + String[] arguments = new String[] {key, member1, member2, GeoUnit.KILOMETERS.getValkeyAPI()}; + Double value = 166.2742; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoDist), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geodist(key, member1, member2, geoUnit); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void geodist_with_metrics_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member1 = gs("Catania"); + GlideString member2 = gs("Palermo"); + GeoUnit geoUnit = GeoUnit.KILOMETERS; + GlideString[] arguments = + new GlideString[] {key, member1, member2, gs(GeoUnit.KILOMETERS.getValkeyAPI())}; + Double value = 166.2742; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoDist), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.geodist(key, member1, member2, geoUnit); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_returns_success() { + // setup + String code = "The best code ever"; + String[] args = new String[] {code}; + String value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, false); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_binary_returns_success() { + // setup + GlideString code = gs("The best code ever"); + GlideString[] args = new GlideString[] {code}; + GlideString value = gs("42"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, false); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_replace_returns_success() { + // setup + String code = "The best code ever"; + String[] args = new String[] {FunctionLoadOptions.REPLACE.toString(), code}; + String value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, true); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_replace_binary_returns_success() { + // setup + GlideString code = gs("The best code ever"); + GlideString[] args = new GlideString[] {gs(FunctionLoadOptions.REPLACE.toString()), code}; + GlideString value = gs("42"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, true); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_returns_success() { + // setup + String[] args = new String[0]; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(false); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_binary_returns_success() { + // setup + GlideString[] args = new GlideString[0]; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionListBinary(false); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_with_pattern_returns_success() { + // setup + String pattern = "*"; + String[] args = new String[] {LIBRARY_NAME_VALKEY_API, pattern, WITH_CODE_VALKEY_API}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(pattern, true); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_binary_with_pattern_returns_success() { + // setup + GlideString pattern = gs("*"); + GlideString[] args = + new GlideString[] {gs(LIBRARY_NAME_VALKEY_API), pattern, gs(WITH_CODE_VALKEY_API)}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = + service.functionListBinary(pattern, true); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionFlush_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_mode_returns_success() { + // setup + FlushMode mode = ASYNC; + String[] args = new String[] {mode.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(mode); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionDelete_returns_success() { + // setup + String libName = "GLIDE"; + String[] args = new String[] {libName}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionDelete_binary_returns_success() { + // setup + GlideString libName = gs("GLIDE"); + GlideString[] args = new GlideString[] {libName}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void fcall_with_keys_and_args_returns_success() { + // setup + String function = "func"; + String[] keys = new String[] {"key1", "key2"}; + String[] arguments = new String[] {"1", "2"}; + String[] args = new String[] {function, "2", "key1", "key2", "1", "2"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function, keys, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_with_keys_and_args_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = + new GlideString[] {function, gs("2"), gs("key1"), gs("key2"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function, keys, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_returns_success() { + // setup + String function = "func"; + String[] args = new String[] {function, "0"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_with_keys_and_args_returns_success() { + // setup + String function = "func"; + String[] keys = new String[] {"key1", "key2"}; + String[] arguments = new String[] {"1", "2"}; + String[] args = new String[] {function, "2", "key1", "key2", "1", "2"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function, keys, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_with_keys_and_args_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = + new GlideString[] {function, gs("2"), gs("key1"), gs("key2"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function, keys, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_returns_success() { + // setup + String function = "func"; + String[] args = new String[] {function, "0"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_binary_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionKill_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionKill), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionKill(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionStats_returns_success() { + // setup + String[] args = new String[0]; + Map> value = Map.of("1", Map.of("2", 2)); + CompletableFuture>> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(FunctionStats), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>> response = service.functionStats(); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionStatsBinary_returns_success() { + // setup + GlideString[] args = new GlideString[0]; + Map> value = Map.of(gs("1"), Map.of(gs("2"), 2)); + CompletableFuture>> testResponse = + new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>>submitNewCommand( + eq(FunctionStats), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>> response = + service.functionStatsBinary(); + Map> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionDump_returns_success() { + // setup + byte[] value = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDump), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDump(); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionRestore_returns_success() { + // setup + byte[] data = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FunctionRestore), eq(new GlideString[] {gs(data)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, FunctionRestorePolicy.FLUSH); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void bitcount_returns_success() { + // setup + String key = "testKey"; + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitCount), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(1L, payload); + assertEquals(bitcount, payload); + } + + @SneakyThrows + @Test + public void bitcount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitCount), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(1L, payload); + assertEquals(bitcount, payload); + } + + @SneakyThrows + @Test + public void bitcount_indices_returns_success() { + // setup + String key = "testKey"; + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new String[] {key, "1", "2"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + + @SneakyThrows + @Test + public void bitcount_indices_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new GlideString[] {key, gs("1"), gs("2")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + + @SneakyThrows + @Test + public void bitcount_indices_with_option_returns_success() { + // setup + String key = "testKey"; + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new String[] {key, "1", "2", "BIT"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + + @SneakyThrows + @Test + public void bitcount_indices_with_option_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new GlideString[] {key, gs("1"), gs("2"), gs("BIT")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + + @SneakyThrows + @Test + public void setbit_returns_success() { + // setup + String key = "testKey"; + Long value = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SetBit), eq(new String[] {key, "8", "1"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.setbit(key, 8, 1); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void setbit_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long value = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(SetBit), eq(new GlideString[] {key, gs("8"), gs("1")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.setbit(key, 8, 1); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blmpop_returns_success() { + // setup + String key = "testKey"; + String key2 = "testKey2"; + String[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + double timeout = 0.1; + String[] arguments = + new String[] {Double.toString(timeout), "2", key, key2, listDirection.toString()}; + Map value = Map.of(key, new String[] {"five"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(BLMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.blmpop(keys, listDirection, timeout); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blmpop_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString key2 = gs("testKey2"); + GlideString[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + double timeout = 0.1; + GlideString[] arguments = + new GlideString[] { + gs(Double.toString(timeout)), gs("2"), key, key2, gs(listDirection.toString()) + }; + Map value = Map.of(key, new GlideString[] {gs("five")}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(BLMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.blmpop(keys, listDirection, timeout); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blmpop_with_count_returns_success() { + // setup + String key = "testKey"; + String key2 = "testKey2"; + String[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + long count = 1L; + double timeout = 0.1; + String[] arguments = + new String[] { + Double.toString(timeout), + "2", + key, + key2, + listDirection.toString(), + COUNT_FOR_LIST_VALKEY_API, + Long.toString(count) + }; + Map value = Map.of(key, new String[] {"five"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(BLMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.blmpop(keys, listDirection, count, timeout); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blmpop_with_count_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString key2 = gs("testKey2"); + GlideString[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + long count = 1L; + double timeout = 0.1; + GlideString[] arguments = + new GlideString[] { + gs(Double.toString(timeout)), + gs("2"), + key, + key2, + gs(listDirection.toString()), + gs(COUNT_FOR_LIST_VALKEY_API), + gs(Long.toString(count)) + }; + Map value = Map.of(key, new GlideString[] {gs("five")}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(BLMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.blmpop(keys, listDirection, count, timeout); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void getbit_returns_success() { + // setup + String key = "testKey"; + Long bit = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bit); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GetBit), eq(new String[] {key, "8"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getbit(key, 8); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bit, payload); + } + + @SneakyThrows + @Test + public void getbit_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bit = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bit); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(GetBit), eq(new GlideString[] {key, gs("8")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.getbit(key, 8); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bit, payload); + } + + @SneakyThrows + @Test + public void bitpos_returns_success() { + // setup + String key = "testKey"; + Long bit = 0L; + Long bitPosition = 10L; + String[] arguments = new String[] {key, Long.toString(bit)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + + @SneakyThrows + @Test + public void bitpos_with_start_returns_success() { + // setup + String key = "testKey"; + Long bit = 0L; + Long start = 5L; + Long bitPosition = 10L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitPos), eq(new String[] {key, Long.toString(bit), Long.toString(start)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit, start); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + + @SneakyThrows + @Test + public void bitpos_with_start_and_end_returns_success() { + // setup + String key = "testKey"; + Long bit = 0L; + Long start = 5L; + Long end = 10L; + Long bitPosition = 10L; + String[] arguments = + new String[] {key, Long.toString(bit), Long.toString(start), Long.toString(end)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit, start, end); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + + @SneakyThrows + @Test + public void bitpos_with_start_and_end_and_type_returns_success() { + // setup + String key = "testKey"; + Long bit = 0L; + Long start = 5L; + Long end = 10L; + Long bitPosition = 10L; + String[] arguments = + new String[] { + key, + Long.toString(bit), + Long.toString(start), + Long.toString(end), + BitmapIndexType.BIT.toString() + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit, start, end, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + + @SneakyThrows + @Test + public void bitpos_with_start_and_end_and_type_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bit = 0L; + Long start = 5L; + Long end = 10L; + Long bitPosition = 10L; + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit)), + gs(Long.toString(start)), + gs(Long.toString(end)), + gs(BitmapIndexType.BIT.toString()) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit, start, end, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + + @SneakyThrows + @Test + public void bitop_returns_success() { + // setup + String destination = "destination"; + String[] keys = new String[] {"key1", "key2"}; + Long result = 6L; + BitwiseOperation bitwiseAnd = BitwiseOperation.AND; + String[] arguments = concatenateArrays(new String[] {bitwiseAnd.toString(), destination}, keys); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitOp), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitop(bitwiseAnd, destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void bitop_bianry_returns_success() { + // setup + GlideString destination = gs("destination"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Long result = 6L; + BitwiseOperation bitwiseAnd = BitwiseOperation.AND; + GlideString[] arguments = + concatenateArrays(new GlideString[] {gs(bitwiseAnd.toString()), destination}, keys); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitOp), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitop(bitwiseAnd, destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void lmpop_returns_success() { + // setup + String key = "testKey"; + String key2 = "testKey2"; + String[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + String[] arguments = new String[] {"2", key, key2, listDirection.toString()}; + Map value = Map.of(key, new String[] {"five"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lmpop(keys, listDirection); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lmpop_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString key2 = gs("testKey2"); + GlideString[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + GlideString[] arguments = new GlideString[] {gs("2"), key, key2, gs(listDirection.toString())}; + Map value = Map.of(key, new GlideString[] {gs("five")}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.lmpop(keys, listDirection); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lmpop_with_count_returns_success() { + // setup + String key = "testKey"; + String key2 = "testKey2"; + String[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + long count = 1L; + String[] arguments = + new String[] { + "2", key, key2, listDirection.toString(), COUNT_FOR_LIST_VALKEY_API, Long.toString(count) + }; + Map value = Map.of(key, new String[] {"five"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lmpop(keys, listDirection, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lmpop_with_count_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString key2 = gs("testKey2"); + GlideString[] keys = {key, key2}; + ListDirection listDirection = ListDirection.LEFT; + long count = 1L; + GlideString[] arguments = + new GlideString[] { + gs("2"), + key, + key2, + gs(listDirection.toString()), + gs(COUNT_FOR_LIST_VALKEY_API), + gs(Long.toString(count)) + }; + Map value = Map.of(key, new GlideString[] {gs("five")}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.lmpop(keys, listDirection, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lmove_returns_success() { + // setup + String key1 = "testKey"; + String key2 = "testKey2"; + ListDirection wherefrom = ListDirection.LEFT; + ListDirection whereto = ListDirection.RIGHT; + String[] arguments = new String[] {key1, key2, wherefrom.toString(), whereto.toString()}; + String value = "one"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lmove(key1, key2, wherefrom, whereto); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lmove_binary_returns_success() { + // setup + GlideString key1 = gs("testKey"); + GlideString key2 = gs("testKey2"); + ListDirection wherefrom = ListDirection.LEFT; + ListDirection whereto = ListDirection.RIGHT; + GlideString[] arguments = + new GlideString[] {key1, key2, gs(wherefrom.toString()), gs(whereto.toString())}; + GlideString value = gs("one"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lmove(key1, key2, wherefrom, whereto); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lset_returns_success() { + // setup + String key = "testKey"; + long index = 0; + String element = "two"; + String[] arguments = new String[] {key, "0", element}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LSet), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lset(key, index, element); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void lset_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long index = 0; + GlideString element = gs("two"); + GlideString[] arguments = new GlideString[] {key, gs("0"), element}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LSet), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lset(key, index, element); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void blmove_returns_success() { + // setup + String key1 = "testKey"; + String key2 = "testKey2"; + ListDirection wherefrom = ListDirection.LEFT; + ListDirection whereto = ListDirection.RIGHT; + String[] arguments = new String[] {key1, key2, wherefrom.toString(), whereto.toString(), "0.1"}; + String value = "one"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BLMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.blmove(key1, key2, wherefrom, whereto, 0.1); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void blmove_binary_returns_success() { + // setup + GlideString key1 = gs("testKey"); + GlideString key2 = gs("testKey2"); + ListDirection wherefrom = ListDirection.LEFT; + ListDirection whereto = ListDirection.RIGHT; + GlideString[] arguments = + new GlideString[] {key1, key2, gs(wherefrom.toString()), gs(whereto.toString()), gs("0.1")}; + GlideString value = gs("one"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BLMove), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.blmove(key1, key2, wherefrom, whereto, 0.1); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sintercard_returns_success() { + // setup + String key1 = "testKey"; + String key2 = "testKey2"; + String[] arguments = new String[] {"2", key1, key2}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sintercard(new String[] {key1, key2}); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sintercard_binary_returns_success() { + // setup + GlideString key1 = gs("testKey"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {gs("2"), key1, key2}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sintercard(new GlideString[] {key1, key2}); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sintercard_with_limit_returns_success() { + // setup + String key1 = "testKey"; + String key2 = "testKey2"; + long limit = 1L; + String[] arguments = new String[] {"2", key1, key2, SET_LIMIT_VALKEY_API, "1"}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sintercard(new String[] {key1, key2}, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sintercard_with_limit_binary_returns_success() { + // setup + GlideString key1 = gs("testKey"); + GlideString key2 = gs("testKey2"); + long limit = 1L; + GlideString[] arguments = + new GlideString[] {gs("2"), key1, key2, gs(SET_LIMIT_VALKEY_API), gs("1")}; + Long value = 1L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sintercard(new GlideString[] {key1, key2}, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void srandmember_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + String value = "one"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srandmember(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void srandmember_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + GlideString value = gs("one"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srandmember(key); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void srandmember_with_count_returns_success() { + // setup + String key = "testKey"; + long count = 2; + String[] arguments = new String[] {key, Long.toString(count)}; + String[] value = {"one", "two"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srandmember(key, count); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void srandmember_with_count_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + GlideString[] value = {gs("one"), gs("two")}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SRandMember), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.srandmember(key, count); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void spop_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + String value = "value"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.spop(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void spop_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + GlideString value = gs("value"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.spop(key); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void spopCount_returns_success() { + // setup + String key = "testKey"; + long count = 2; + String[] arguments = new String[] {key, Long.toString(count)}; + Set value = Set.of("one", "two"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.spopCount(key, count); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void spopCount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(count))}; + Set value = Set.of(gs("one"), gs("two")); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.spopCount(key, count); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void bitfieldReadOnly_returns_success() { + // setup + String key = "testKey"; + Long[] result = new Long[] {7L, 8L}; + Offset offset = new Offset(1); + OffsetMultiplier offsetMultiplier = new OffsetMultiplier(8); + BitFieldGet subcommand1 = new BitFieldGet(new UnsignedEncoding(4), offset); + BitFieldGet subcommand2 = new BitFieldGet(new SignedEncoding(5), offsetMultiplier); + String[] args = { + key, + BitFieldOptions.GET_COMMAND_STRING, + BitFieldOptions.UNSIGNED_ENCODING_PREFIX.concat("4"), + offset.getOffset(), + BitFieldOptions.GET_COMMAND_STRING, + BitFieldOptions.SIGNED_ENCODING_PREFIX.concat("5"), + offsetMultiplier.getOffset() + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitFieldReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.bitfieldReadOnly(key, new BitFieldReadOnlySubCommands[] {subcommand1, subcommand2}); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void bitfieldReadOnly_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long[] result = new Long[] {7L, 8L}; + Offset offset = new Offset(1); + OffsetMultiplier offsetMultiplier = new OffsetMultiplier(8); + BitFieldGet subcommand1 = new BitFieldGet(new UnsignedEncoding(4), offset); + BitFieldGet subcommand2 = new BitFieldGet(new SignedEncoding(5), offsetMultiplier); + GlideString[] args = { + key, + gs(BitFieldOptions.GET_COMMAND_STRING), + gs(BitFieldOptions.UNSIGNED_ENCODING_PREFIX.concat("4")), + gs(offset.getOffset()), + gs(BitFieldOptions.GET_COMMAND_STRING), + gs(BitFieldOptions.SIGNED_ENCODING_PREFIX.concat("5")), + gs(offsetMultiplier.getOffset()) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitFieldReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.bitfieldReadOnly(key, new BitFieldReadOnlySubCommands[] {subcommand1, subcommand2}); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void bitfield_returns_success() { + // setup + String key = "testKey"; + Long[] result = new Long[] {7L, 8L, 9L}; + UnsignedEncoding u2 = new UnsignedEncoding(2); + SignedEncoding i8 = new SignedEncoding(8); + Offset offset = new Offset(1); + OffsetMultiplier offsetMultiplier = new OffsetMultiplier(8); + long setValue = 2; + long incrbyValue = 5; + String[] args = + new String[] { + key, + SET_COMMAND_STRING, + u2.getEncoding(), + offset.getOffset(), + Long.toString(setValue), + GET_COMMAND_STRING, + i8.getEncoding(), + offsetMultiplier.getOffset(), + OVERFLOW_COMMAND_STRING, + SAT.toString(), + INCRBY_COMMAND_STRING, + u2.getEncoding(), + offset.getOffset(), + Long.toString(incrbyValue) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.bitfield( + key, + new BitFieldSubCommands[] { + new BitFieldSet(u2, offset, setValue), + new BitFieldGet(i8, offsetMultiplier), + new BitFieldOptions.BitFieldOverflow(SAT), + new BitFieldOptions.BitFieldIncrby(u2, offset, incrbyValue), + }); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void bitfield_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long[] result = new Long[] {7L, 8L, 9L}; + UnsignedEncoding u2 = new UnsignedEncoding(2); + SignedEncoding i8 = new SignedEncoding(8); + Offset offset = new Offset(1); + OffsetMultiplier offsetMultiplier = new OffsetMultiplier(8); + long setValue = 2; + long incrbyValue = 5; + GlideString[] args = + new GlideString[] { + key, + gs(SET_COMMAND_STRING), + gs(u2.getEncoding()), + gs(offset.getOffset()), + gs(Long.toString(setValue)), + gs(GET_COMMAND_STRING), + gs(i8.getEncoding()), + gs(offsetMultiplier.getOffset()), + gs(OVERFLOW_COMMAND_STRING), + gs(SAT.toString()), + gs(INCRBY_COMMAND_STRING), + gs(u2.getEncoding()), + gs(offset.getOffset()), + gs(Long.toString(incrbyValue)) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.bitfield( + key, + new BitFieldSubCommands[] { + new BitFieldSet(u2, offset, setValue), + new BitFieldGet(i8, offsetMultiplier), + new BitFieldOptions.BitFieldOverflow(SAT), + new BitFieldOptions.BitFieldIncrby(u2, offset, incrbyValue), + }); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void move_returns_success() { + // setup + String key = "testKey"; + long dbIndex = 2L; + String[] arguments = new String[] {key, Long.toString(dbIndex)}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Move), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.move(key, dbIndex); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void move_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long dbIndex = 2L; + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(dbIndex))}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Move), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.move(key, dbIndex); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void copy_returns_success() { + // setup + String source = "testKey1"; + String destination = "testKey2"; + String[] arguments = new String[] {source, destination}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void copy_binary_returns_success() { + // setup + GlideString source = gs("testKey1"); + GlideString destination = gs("testKey2"); + GlideString[] arguments = new GlideString[] {source, destination}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void copy_with_replace_returns_success() { + // setup + String source = "testKey1"; + String destination = "testKey2"; + String[] arguments = new String[] {source, destination, REPLACE_VALKEY_API}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination, true); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void copy_with_destinationDB_returns_success() { + // setup + String source = "testKey1"; + String destination = "testKey2"; + long destinationDB = 1; + String[] arguments = new String[] {source, destination, DB_VALKEY_API, "1", REPLACE_VALKEY_API}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination, destinationDB, true); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcs() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2}; + String value = "foo"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lcs(key1, key2); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcs_binary() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {key1, key2}; + GlideString value = gs("foo"); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lcs(key1, key2); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcs_with_len_option() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2, LEN_VALKEY_API}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lcsLen(key1, key2); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcs_with_len_option_binary() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {key1, key2, gs(LEN_VALKEY_API)}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lcsLen(key1, key2); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdx() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2, IDX_COMMAND_STRING}; + Map value = Map.of("matches", new Long[][][] {{{1L, 3L}, {0L, 2L}}}, "len", 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdx(key1, key2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdx_binary() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {key1, key2, gs(IDX_COMMAND_STRING)}; + Map value = Map.of("matches", new Long[][][] {{{1L, 3L}, {0L, 2L}}}, "len", 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdx(key1, key2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdx_throws_NullPointerException() { + // setup + Map value = Map.of("missing", new Long[][][] {{{1L, 3L}, {0L, 2L}}}, "len", 3L); + + // exception + RuntimeException runtimeException = + assertThrows(RuntimeException.class, () -> service.handleLcsIdxResponse(value)); + assertInstanceOf(NullPointerException.class, runtimeException); + assertEquals( + "LCS result does not contain the key \"" + LCS_MATCHES_RESULT_KEY + "\"", + runtimeException.getMessage()); + } + + @SneakyThrows + @Test + public void lcsIdx_with_options() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = + new String[] {key1, key2, IDX_COMMAND_STRING, MINMATCHLEN_COMMAND_STRING, "2"}; + Map value = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdx(key1, key2, 2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdx_with_options_binary() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = + new GlideString[] { + key1, key2, gs(IDX_COMMAND_STRING), gs(MINMATCHLEN_COMMAND_STRING), gs("2") + }; + Map value = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdx(key1, key2, 2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdxWithMatchLen() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2, IDX_COMMAND_STRING, WITHMATCHLEN_COMMAND_STRING}; + Map value = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdxWithMatchLen(key1, key2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdxWithMatchLen_binary() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = + new GlideString[] {key1, key2, gs(IDX_COMMAND_STRING), gs(WITHMATCHLEN_COMMAND_STRING)}; + Map value = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdxWithMatchLen(key1, key2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdxWithMatchLen_with_options() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = + new String[] { + key1, + key2, + IDX_COMMAND_STRING, + MINMATCHLEN_COMMAND_STRING, + "2", + WITHMATCHLEN_COMMAND_STRING + }; + Map value = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdxWithMatchLen(key1, key2, 2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcsIdxWithMatchLen_with_options_binary() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = + new GlideString[] { + key1, + key2, + gs(IDX_COMMAND_STRING), + gs(MINMATCHLEN_COMMAND_STRING), + gs("2"), + gs(WITHMATCHLEN_COMMAND_STRING) + }; + Map value = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lcsIdxWithMatchLen(key1, key2, 2); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void watch_returns_success() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Watch), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.watch(arguments); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void watch_binary_returns_success() { + // setup + GlideString key1 = gs("testKey1"); + GlideString key2 = gs("testKey2"); + GlideString[] arguments = new GlideString[] {key1, key2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Watch), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.watch(arguments); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void unwatch_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(UnWatch), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.unwatch(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void publish_returns_success() { + // setup + String channel = "channel"; + String message = "message"; + String[] arguments = new String[] {channel, message}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Publish), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.publish(message, channel); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void sunion_returns_success() { + // setup + String[] keys = new String[] {"key1", "key2"}; + Set value = Set.of("1", "2"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SUnion), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sunion(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sunion_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Set value = Set.of(gs("1"), gs("2")); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SUnion), eq(keys), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.sunion(keys); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void dump_returns_success() { + // setup + GlideString key = gs("testKey"); + byte[] value = "value".getBytes(); + GlideString[] arguments = new GlideString[] {key}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Dump), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.dump(key); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void restore_returns_success() { + // setup + GlideString key = gs("testKey"); + long ttl = 0L; + byte[] value = "value".getBytes(); + + GlideString[] arg = new GlideString[] {key, gs(Long.toString(ttl).getBytes()), gs(value)}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Restore), eq(arg), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.restore(key, ttl, value); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void restore_with_restoreOptions_returns_success() { + // setup + GlideString key = gs("testKey"); + long ttl = 0L; + byte[] value = "value".getBytes(); + Long idletime = 10L; + Long frequency = 5L; + + GlideString[] arg = + new GlideString[] { + key, + gs(Long.toString(ttl)), + gs(value), + gs("REPLACE"), + gs("ABSTTL"), + gs("IDLETIME"), + gs("10"), + gs("FREQ"), + gs("5") + }; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Restore), eq(arg), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.restore( + key, + ttl, + value, + RestoreOptions.builder().replace().absttl().idletime(10L).frequency(5L).build()); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_options_binary_returns_success() { + // setup + GlideString[] result = new GlideString[] {gs("1"), gs("2"), gs("3")}; + GlideString key = gs("key"); + Long limitOffset = 0L; + Long limitCount = 2L; + GlideString byPattern = gs("byPattern"); + GlideString getPattern = gs("getPattern"); + GlideString[] args = + new GlideString[] { + key, + gs(LIMIT_COMMAND_STRING), + gs(limitOffset.toString()), + gs(limitCount.toString()), + gs(DESC.toString()), + gs(ALPHA_COMMAND_STRING), + BY_COMMAND_GLIDE_STRING, + byPattern, + GET_COMMAND_GLIDE_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortOptionsBinary.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sscan_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = new String[] {key, cursor}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scan_returns_success() { + // setup + String cursor = "0"; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Scan), eq(new String[] {cursor}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scan(cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scan_binary_returns_success() { + // setup + GlideString cursor = gs("0"); + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Scan), eq(new GlideString[] {cursor}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scan(cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scan_with_options_returns_success() { + // setup + String cursor = "0"; + ScanOptions options = + ScanOptions.builder().matchPattern("match").count(10L).type(STRING).build(); + String[] args = + new String[] { + cursor, + MATCH_OPTION_STRING, + "match", + COUNT_OPTION_STRING, + "10", + TYPE_OPTION_STRING, + STRING.toString() + }; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Scan), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scan(cursor, options); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void scan_binary_with_options_returns_success() { + // setup + GlideString cursor = gs("0"); + ScanOptions options = + ScanOptions.builder().matchPattern("match").count(10L).type(STRING).build(); + GlideString[] args = + new GlideString[] { + cursor, + gs(MATCH_OPTION_STRING), + gs("match"), + gs(COUNT_OPTION_STRING), + gs("10"), + gs(TYPE_OPTION_STRING), + gs(STRING.toString()) + }; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Scan), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scan(cursor, options); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sscan_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString cursor = gs("0"); + GlideString[] arguments = new GlideString[] {key, cursor}; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] {key, BY_COMMAND_STRING, byPattern, GET_COMMAND_STRING, getPattern}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, SortOptions.builder().getPattern(getPattern).byPattern(byPattern).build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_binary_returns_success() { + // setup + GlideString[] result = new GlideString[] {gs("1"), gs("2"), gs("3")}; + GlideString key = gs("key"); + GlideString byPattern = gs("byPattern"); + GlideString getPattern = gs("getPattern"); + GlideString[] args = + new GlideString[] { + key, BY_COMMAND_GLIDE_STRING, byPattern, GET_COMMAND_GLIDE_STRING, getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, SortOptionsBinary.builder().getPattern(getPattern).byPattern(byPattern).build()); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern, + STORE_COMMAND_STRING, + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_binary_returns_success() { + // setup + Long result = 5L; + GlideString key = gs("key"); + GlideString destKey = gs("destKey"); + Long limitOffset = 0L; + Long limitCount = 2L; + GlideString byPattern = gs("byPattern"); + GlideString getPattern = gs("getPattern"); + GlideString[] args = + new GlideString[] { + key, + gs(LIMIT_COMMAND_STRING), + gs(limitOffset.toString()), + gs(limitCount.toString()), + gs(DESC.toString()), + gs(ALPHA_COMMAND_STRING), + gs(BY_COMMAND_STRING), + byPattern, + GET_COMMAND_GLIDE_STRING, + getPattern, + gs(STORE_COMMAND_STRING), + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortOptionsBinary.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void wait_returns_success() { + // setup + long numreplicas = 1L; + long timeout = 1000L; + Long result = 5L; + String[] args = new String[] {"1", "1000"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Wait), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.wait(numreplicas, timeout); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sscan_with_options_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = + new String[] {key, cursor, MATCH_OPTION_STRING, "*", COUNT_OPTION_STRING, "1"}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sscan(key, cursor, SScanOptions.builder().matchPattern("*").count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void sscan_with_options_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString cursor = gs("0"); + GlideString[] arguments = + new GlideString[] { + key, cursor, MATCH_OPTION_GLIDE_STRING, gs("*"), COUNT_OPTION_GLIDE_STRING, gs("1") + }; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sscan( + key, cursor, SScanOptionsBinary.builder().matchPattern(gs("*")).count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscan_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = new String[] {key, cursor}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscan_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString cursor = gs("0"); + GlideString[] arguments = new GlideString[] {key, cursor}; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscan_with_options_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = + new String[] {key, cursor, MATCH_OPTION_STRING, "*", COUNT_OPTION_STRING, "1"}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zscan(key, cursor, ZScanOptions.builder().matchPattern("*").count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void zscan_with_options_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString cursor = gs("0"); + GlideString[] arguments = + new GlideString[] { + key, cursor, MATCH_OPTION_GLIDE_STRING, gs("*"), COUNT_OPTION_GLIDE_STRING, gs("1") + }; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.zscan( + key, cursor, ZScanOptionsBinary.builder().matchPattern(gs("*")).count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hscan_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = new String[] {key, cursor}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hscan_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString cursor = gs("0"); + GlideString[] arguments = new GlideString[] {key, cursor}; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hscan(key, cursor); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hscan_with_options_returns_success() { + // setup + String key = "testKey"; + String cursor = "0"; + String[] arguments = + new String[] {key, cursor, MATCH_OPTION_STRING, "*", COUNT_OPTION_STRING, "1"}; + Object[] value = new Object[] {0L, new String[] {"hello", "world"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.hscan(key, cursor, HScanOptions.builder().matchPattern("*").count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void hscan_with_options_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString cursor = gs("0"); + GlideString[] arguments = + new GlideString[] { + key, cursor, MATCH_OPTION_GLIDE_STRING, gs("*"), COUNT_OPTION_GLIDE_STRING, gs("1") + }; + Object[] value = new Object[] {0L, new GlideString[] {gs("hello"), gs("world")}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HScan), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.hscan( + key, cursor, HScanOptionsBinary.builder().matchPattern(gs("*")).count(1L).build()); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + private static List getGeoSearchArguments() { + return List.of( + Arguments.of( + "geosearch_from_member_no_options", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + null, + new String[] { + "GeoSearchTestKey", FROMMEMBER_VALKEY_API, "member", "BYRADIUS", "1.0", "km" + }, + new String[] {"place1", "place2"}), + Arguments.of( + "geosearch_from_member_ASC", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC), + new String[] { + "GeoSearchTestKey", + FROMMEMBER_VALKEY_API, + "member", + "BYBOX", + "1.0", + "1.0", + "km", + "ASC" + }, + new String[] {"place2", "place1"}), + Arguments.of( + "geosearch_from_lonlat_with_count", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(2), + new String[] { + "GeoSearchTestKey", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYRADIUS", + "1.0", + "km", + COUNT_VALKEY_API, + "2" + }, + new String[] {"place3", "place4"}), + Arguments.of( + "geosearch_from_lonlat_with_count_any_DESC", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.DESC, 2, true), + new String[] { + "GeoSearchTestKey", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYBOX", + "1.0", + "1.0", + "km", + COUNT_VALKEY_API, + "2", + "ANY", + "DESC" + }, + new String[] {"place4", "place3"})); + } + + private static List getGeoSearchArgumentsBinary() { + return List.of( + Arguments.of( + "geosearch_from_member_no_options", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + null, + new GlideString[] { + gs("GeoSearchTestKey"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYRADIUS"), + gs("1.0"), + gs("km") + }, + new GlideString[] {gs("place1"), gs("place2")}), + Arguments.of( + "geosearch_from_member_ASC", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC), + new GlideString[] { + gs("GeoSearchTestKey"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYBOX"), + gs("1.0"), + gs("1.0"), + gs("km"), + gs("ASC") + }, + new GlideString[] {gs("place2"), gs("place1")}), + Arguments.of( + "geosearch_from_lonlat_with_count", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(2), + new GlideString[] { + gs("GeoSearchTestKey"), + gs(FROMLONLAT_VALKEY_API), + gs("1.0"), + gs("1.0"), + gs("BYRADIUS"), + gs("1.0"), + gs("km"), + gs(COUNT_VALKEY_API), + gs("2") + }, + new GlideString[] {gs("place3"), gs("place4")}), + Arguments.of( + "geosearch_from_lonlat_with_count_any_DESC", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.DESC, 2, true), + new GlideString[] { + gs("GeoSearchTestKey"), + gs(FROMLONLAT_VALKEY_API), + gs("1.0"), + gs("1.0"), + gs("BYBOX"), + gs("1.0"), + gs("1.0"), + gs("km"), + gs(COUNT_VALKEY_API), + gs("2"), + gs("ANY"), + gs("DESC") + }, + new GlideString[] {gs("place4"), gs("place3")})); + } + + @SneakyThrows + @ParameterizedTest(name = "{1}") + @MethodSource("getGeoSearchArguments") + public void geosearch_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchResultOptions resultOptions, + String[] args, + String[] expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearch), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearch("GeoSearchTestKey", origin, shape) + : service.geosearch("GeoSearchTestKey", origin, shape, resultOptions); + String[] payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertArrayEquals(expected, payload); + } + + @SneakyThrows + @ParameterizedTest(name = "{1}") + @MethodSource("getGeoSearchArgumentsBinary") + public void geosearch_binary_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchResultOptions resultOptions, + GlideString[] args, + GlideString[] expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearch), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearch(gs("GeoSearchTestKey"), origin, shape) + : service.geosearch(gs("GeoSearchTestKey"), origin, shape, resultOptions); + GlideString[] payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertArrayEquals(expected, payload); + } + + private static List getGeoSearchWithOptionsArguments() { + return List.of( + Arguments.of( + "geosearch_from_member_with_options", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withcoord().withdist().withhash().build(), + null, + new String[] { + "GeoSearchTestKey", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km", + "WITHDIST", + "WITHCOORD", + "WITHHASH" + }, + new Object[] { + new Object[] { + "Catania", + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + "Palermo", + new Object[] { + 190.4424, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + } + }), + Arguments.of( + "geosearch_from_member_with_options_and_SORT_and_COUNT_ANY", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withcoord().withdist().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2, true), + new String[] { + "GeoSearchTestKey", + FROMMEMBER_VALKEY_API, + "member", + "BYBOX", + "1.0", + "1.0", + "km", + "WITHDIST", + "WITHCOORD", + "WITHHASH", + "COUNT", + "2", + "ANY", + "ASC" + }, + new Object[] { + new Object[] { + "Catania", + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + } + })); + } + + private static List getGeoSearchWithOptionsArgumentsBinary() { + return List.of( + Arguments.of( + "geosearch_from_member_with_options", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withcoord().withdist().withhash().build(), + null, + new GlideString[] { + gs("GeoSearchTestKey"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYRADIUS"), + gs("1.0"), + gs("km"), + gs("WITHDIST"), + gs("WITHCOORD"), + gs("WITHHASH") + }, + new Object[] { + new Object[] { + gs("Catania"), + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + gs("Palermo"), + new Object[] { + 190.4424, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + } + }), + Arguments.of( + "geosearch_from_member_with_options_and_SORT_and_COUNT_ANY", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withcoord().withdist().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2, true), + new GlideString[] { + gs("GeoSearchTestKey"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYBOX"), + gs("1.0"), + gs("1.0"), + gs("km"), + gs("WITHDIST"), + gs("WITHCOORD"), + gs("WITHHASH"), + gs("COUNT"), + gs("2"), + gs("ANY"), + gs("ASC") + }, + new Object[] { + new Object[] { + gs("Catania"), + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + } + })); + } + + @SneakyThrows + @ParameterizedTest(name = "{2}") + @MethodSource("getGeoSearchWithOptionsArguments") + public void geosearch_with_options_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchOptions options, + GeoSearchResultOptions resultOptions, + String[] args, + Object[] expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearch), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearch("GeoSearchTestKey", origin, shape, options) + : service.geosearch("GeoSearchTestKey", origin, shape, options, resultOptions); + Object[] payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertArrayEquals(expected, payload); + } + + @SneakyThrows + @ParameterizedTest(name = "{2}") + @MethodSource("getGeoSearchWithOptionsArgumentsBinary") + public void geosearch_with_options_binary_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchOptions options, + GeoSearchResultOptions resultOptions, + GlideString[] args, + Object[] expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearch), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearch(gs("GeoSearchTestKey"), origin, shape, options) + : service.geosearch(gs("GeoSearchTestKey"), origin, shape, options, resultOptions); + Object[] payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertArrayEquals(expected, payload); + } + + private static List getGeoSearchStoreArguments() { + return List.of( + Arguments.of( + "geosearchstore_from_member_no_options", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + null, + new String[] { + "testDestination", + "testSource", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km" + }, + 1L), + Arguments.of( + "geosearchstore_from_member_ASC", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC), + new String[] { + "testDestination", + "testSource", + FROMMEMBER_VALKEY_API, + "member", + "BYBOX", + "1.0", + "1.0", + "km", + "ASC" + }, + 2L), + Arguments.of( + "geosearchstore_from_lonlat_with_count", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(2), + new String[] { + "testDestination", + "testSource", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYRADIUS", + "1.0", + "km", + COUNT_VALKEY_API, + "2" + }, + 3L), + Arguments.of( + "geosearchstore_from_lonlat_with_count_any_DESC", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.DESC, 2, true), + new String[] { + "testDestination", + "testSource", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYBOX", + "1.0", + "1.0", + "km", + COUNT_VALKEY_API, + "2", + "ANY", + "DESC" + }, + 4L)); + } + + private static List getGeoSearchStoreArgumentsBinary() { + return List.of( + Arguments.of( + "geosearchstore_from_member_no_options", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + null, + new GlideString[] { + gs("testDestination"), + gs("testSource"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYRADIUS"), + gs("1.0"), + gs("km") + }, + 1L), + Arguments.of( + "geosearchstore_from_member_ASC", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC), + new GlideString[] { + gs("testDestination"), + gs("testSource"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYBOX"), + gs("1.0"), + gs("1.0"), + gs("km"), + gs("ASC") + }, + 2L), + Arguments.of( + "geosearchstore_from_lonlat_with_count", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(2), + new GlideString[] { + gs("testDestination"), + gs("testSource"), + gs(FROMLONLAT_VALKEY_API), + gs("1.0"), + gs("1.0"), + gs("BYRADIUS"), + gs("1.0"), + gs("km"), + gs(COUNT_VALKEY_API), + gs("2") + }, + 3L), + Arguments.of( + "geosearchstore_from_lonlat_with_count_any_DESC", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.DESC, 2, true), + new GlideString[] { + gs("testDestination"), + gs("testSource"), + gs(FROMLONLAT_VALKEY_API), + gs("1.0"), + gs("1.0"), + gs("BYBOX"), + gs("1.0"), + gs("1.0"), + gs("km"), + gs(COUNT_VALKEY_API), + gs("2"), + gs("ANY"), + gs("DESC") + }, + 4L)); + } + + @SneakyThrows + @ParameterizedTest(name = "{3}") + @MethodSource("getGeoSearchStoreArguments") + public void geosearchstore_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchResultOptions resultOptions, + String[] args, + Long expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearchStore), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearchstore("testDestination", "testSource", origin, shape) + : service.geosearchstore("testDestination", "testSource", origin, shape, resultOptions); + Long payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertEquals(expected, payload); + } + + @SneakyThrows + @ParameterizedTest(name = "{3}") + @MethodSource("getGeoSearchStoreArgumentsBinary") + public void geosearchstore_binary_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchResultOptions resultOptions, + GlideString[] args, + Long expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearchStore), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearchstore(gs("testDestination"), gs("testSource"), origin, shape) + : service.geosearchstore( + gs("testDestination"), gs("testSource"), origin, shape, resultOptions); + Long payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertEquals(expected, payload); + } + + private static List getGeoSearchStoreWithOptionsArguments() { + return List.of( + Arguments.of( + "geosearchstore_from_member_with_options", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().build(), + null, + new String[] { + "testDestination", + "testSource", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km" + }, + 1L), + Arguments.of( + "geosearchstore_from_member_with_options_and_SORT_and_COUNT_ANY", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2, true), + new String[] { + "testDestination", + "testSource", + FROMMEMBER_VALKEY_API, + "member", + "BYBOX", + "1.0", + "1.0", + "km", + "STOREDIST", + "COUNT", + "2", + "ANY", + "ASC" + }, + 2L)); + } + + private static List getGeoSearchStoreWithOptionsArgumentsBinary() { + return List.of( + Arguments.of( + "geosearchstore_from_member_with_options", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().build(), + null, + new GlideString[] { + gs("testDestination"), + gs("testSource"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYRADIUS"), + gs("1.0"), + gs("km") + }, + 1L), + Arguments.of( + "geosearchstore_from_member_with_options_and_SORT_and_COUNT_ANY", + new GeoSearchOrigin.MemberOriginBinary(gs("member")), + new GeoSearchShape(1L, 1L, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2, true), + new GlideString[] { + gs("testDestination"), + gs("testSource"), + gs(FROMMEMBER_VALKEY_API), + gs("member"), + gs("BYBOX"), + gs("1.0"), + gs("1.0"), + gs("km"), + gs("STOREDIST"), + gs("COUNT"), + gs("2"), + gs("ANY"), + gs("ASC") + }, + 2L)); + } + + @SneakyThrows + @ParameterizedTest(name = "{4}") + @MethodSource("getGeoSearchStoreWithOptionsArguments") + public void geosearchstore_with_options_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchStoreOptions options, + GeoSearchResultOptions resultOptions, + String[] args, + Long expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearchStore), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearchstore("testDestination", "testSource", origin, shape, options) + : service.geosearchstore( + "testDestination", "testSource", origin, shape, options, resultOptions); + Long payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertEquals(expected, payload); + } + + @SneakyThrows + @ParameterizedTest(name = "{4}") + @MethodSource("getGeoSearchStoreWithOptionsArgumentsBinary") + public void geosearchstore_with_options_binary_returns_success( + String testName, + GeoSearchOrigin.SearchOrigin origin, + GeoSearchShape shape, + GeoSearchStoreOptions options, + GeoSearchResultOptions resultOptions, + GlideString[] args, + Long expected) { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(expected); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(GeoSearchStore), eq(args), any())) + .thenReturn(testResponse); + + // Exercise + CompletableFuture response = + resultOptions == null + ? service.geosearchstore( + gs("testDestination"), gs("testSource"), origin, shape, options) + : service.geosearchstore( + gs("testDestination"), gs("testSource"), origin, shape, options, resultOptions); + Long payload = response.get(); + + // Verify + assertEquals(testResponse, response); + assertEquals(expected, payload); + } + + @SneakyThrows + @Test + public void xinfoGroups_returns_success() { + // setup + String key = "testKey"; + String[] arguments = {key}; + Map[] mockResult = + new Map[] { + Map.of( + "name", + "groupName", + "consumers", + 2, + "pending", + 2, + "last-delivered-id", + "1638126030001-0", + "entries-read", + 2, + "lag", + 2) + }; + + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(XInfoGroups), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.xinfoGroups(key); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xinfoGroups_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = {key}; + Map[] mockResult = + new Map[] { + Map.of( + gs("name"), + gs("groupName"), + gs("consumers"), + 2, + gs("pending"), + 2, + gs("last-delivered-id"), + gs("1638126030001-0"), + gs("entries-read"), + 2, + gs("lag"), + 2) + }; + + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(XInfoGroups), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.xinfoGroups(key); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xinfoConsumers_returns_success() { + // setup + String key = "testKey"; + String groupName = "groupName"; + String[] arguments = {key, groupName}; + Map[] mockResult = + new Map[] { + Map.of("name", "groupName", "pending", 2, "idle", 9104628, "inactive", 18104698) + }; + + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(XInfoConsumers), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.xinfoConsumers(key, groupName); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xinfoConsumers_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString groupName = gs("groupName"); + GlideString[] arguments = {key, groupName}; + Map[] mockResult = + new Map[] { + Map.of( + gs("name"), + gs("groupName"), + gs("pending"), + 2, + gs("idle"), + 9104628, + gs("inactive"), + 18104698) + }; + + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(mockResult); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(XInfoConsumers), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.xinfoConsumers(key, groupName); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(mockResult, payload); + } + + @SneakyThrows + @Test + public void xinfoStream_returns_success() { + // setup + String key = "testKey"; + String[] arguments = {key}; + Map summary = Map.of("some", "data"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XInfoStream), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xinfoStream(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xinfoStreamFull_returns_success() { + // setup + String key = "testKey"; + String[] arguments = {key, FULL}; + Map summary = Map.of("some", "data"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XInfoStream), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xinfoStreamFull(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xinfoStreamFull_with_count_returns_success() { + // setup + String key = "testKey"; + int count = 42; + String[] arguments = {key, FULL, COUNT, "42"}; + Map summary = Map.of("some", "data"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XInfoStream), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xinfoStreamFull(key, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xinfoStream_glidestring_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = {key}; + Map summary = Map.of(gs("some"), gs("data")); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XInfoStream), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xinfoStream(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xinfoStreamFull_glidestring_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = {key, gs(FULL)}; + Map summary = Map.of(gs("some"), gs("data")); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XInfoStream), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xinfoStreamFull(key); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } + + @SneakyThrows + @Test + public void xinfoStreamFull_glidestring_with_count_returns_success() { + // setup + GlideString key = gs("testKey"); + int count = 42; + GlideString[] arguments = {key, gs(FULL), gs(COUNT), gs("42")}; + Map summary = Map.of(gs("some"), gs("data")); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(summary); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(XInfoStream), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.xinfoStreamFull(key, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(summary, payload); + } +} diff --git a/java/client/src/test/java/glide/api/GlideClusterClientTest.java b/java/client/src/test/java/glide/api/GlideClusterClientTest.java new file mode 100644 index 0000000000..54b49d9de2 --- /dev/null +++ b/java/client/src/test/java/glide/api/GlideClusterClientTest.java @@ -0,0 +1,3214 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api; + +import static command_request.CommandRequestOuterClass.RequestType.ClientGetName; +import static command_request.CommandRequestOuterClass.RequestType.ClientId; +import static command_request.CommandRequestOuterClass.RequestType.ConfigGet; +import static command_request.CommandRequestOuterClass.RequestType.ConfigResetStat; +import static command_request.CommandRequestOuterClass.RequestType.ConfigRewrite; +import static command_request.CommandRequestOuterClass.RequestType.ConfigSet; +import static command_request.CommandRequestOuterClass.RequestType.DBSize; +import static command_request.CommandRequestOuterClass.RequestType.Echo; +import static command_request.CommandRequestOuterClass.RequestType.FCall; +import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.FlushAll; +import static command_request.CommandRequestOuterClass.RequestType.FlushDB; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDelete; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDump; +import static command_request.CommandRequestOuterClass.RequestType.FunctionFlush; +import static command_request.CommandRequestOuterClass.RequestType.FunctionKill; +import static command_request.CommandRequestOuterClass.RequestType.FunctionList; +import static command_request.CommandRequestOuterClass.RequestType.FunctionLoad; +import static command_request.CommandRequestOuterClass.RequestType.FunctionRestore; +import static command_request.CommandRequestOuterClass.RequestType.FunctionStats; +import static command_request.CommandRequestOuterClass.RequestType.Info; +import static command_request.CommandRequestOuterClass.RequestType.LastSave; +import static command_request.CommandRequestOuterClass.RequestType.Lolwut; +import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.RandomKey; +import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Time; +import static command_request.CommandRequestOuterClass.RequestType.UnWatch; +import static glide.api.BaseClient.OK; +import static glide.api.commands.ServerManagementCommands.VERSION_VALKEY_API; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_VALKEY_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_VALKEY_API; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import command_request.CommandRequestOuterClass.CommandRequest; +import glide.api.models.ClusterTransaction; +import glide.api.models.ClusterValue; +import glide.api.models.GlideString; +import glide.api.models.commands.FlushMode; +import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortBaseOptions.Limit; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.function.FunctionLoadOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.scan.ClusterScanCursor; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; +import glide.managers.CommandManager; +import glide.managers.GlideExceptionCheckedFunction; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import response.ResponseOuterClass.ConstantResponse; +import response.ResponseOuterClass.Response; + +public class GlideClusterClientTest { + + GlideClusterClient service; + + CommandManager commandManager; + + private final String[] TEST_ARGS = new String[0]; + + @BeforeEach + public void setUp() { + commandManager = mock(CommandManager.class); + service = + new GlideClusterClient(new BaseClient.ClientBuilder(null, commandManager, null, null)); + } + + @Test + @SneakyThrows + public void custom_command_returns_single_value() { + var commandManager = new TestCommandManager(null); + + try (var client = new TestClient(commandManager, "TEST")) { + var value = client.customCommand(TEST_ARGS).get(); + assertEquals("TEST", value.getSingleValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(TEST_ARGS).get(); + assertEquals(data, value.getMultiValue()); + } + } + + @Test + @SneakyThrows + // test checks that even a map returned as a single value when single node route is used + public void custom_command_with_single_node_route_returns_single_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(TEST_ARGS, RANDOM).get(); + assertEquals(data, value.getSingleValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_with_multi_node_route_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.customCommand(TEST_ARGS, ALL_NODES).get(); + assertEquals(data, value.getMultiValue()); + } + } + + @Test + @SneakyThrows + public void custom_command_returns_single_value_on_constant_response() { + var commandManager = + new TestCommandManager( + Response.newBuilder().setConstantResponse(ConstantResponse.OK).build()); + + try (var client = new TestClient(commandManager, "OK")) { + var value = client.customCommand(TEST_ARGS, ALL_NODES).get(); + assertEquals("OK", value.getSingleValue()); + } + } + + private static class TestClient extends GlideClusterClient { + + private final Object object; + + public TestClient(CommandManager commandManager, Object objectToReturn) { + super(new BaseClient.ClientBuilder(null, commandManager, null, null)); + object = objectToReturn; + } + + @Override + protected T handleValkeyResponse( + Class classType, EnumSet flags, Response response) { + @SuppressWarnings("unchecked") + T returnValue = (T) object; + return returnValue; + } + + @Override + public void close() {} + } + + private static class TestCommandManager extends CommandManager { + + private final Response response; + + public TestCommandManager(Response responseToReturn) { + super(null); + response = responseToReturn != null ? responseToReturn : Response.newBuilder().build(); + } + + @Override + public CompletableFuture submitCommandToChannel( + CommandRequest.Builder command, + GlideExceptionCheckedFunction responseHandler) { + return CompletableFuture.supplyAsync(() -> responseHandler.apply(response)); + } + } + + @SneakyThrows + @Test + public void exec_without_routing() { + // setup + Object[] value = new Object[] {"PONG", "PONG"}; + ClusterTransaction transaction = new ClusterTransaction().ping().ping(); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewTransaction( + eq(transaction), eq(Optional.empty()), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.exec(transaction); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void exec_with_routing() { + // setup + Object[] value = new Object[] {"PONG", "PONG"}; + ClusterTransaction transaction = new ClusterTransaction().ping().ping(); + SingleNodeRoute route = RANDOM; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewTransaction( + eq(transaction), eq(Optional.of(route)), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.exec(transaction, route); + Object[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertArrayEquals(value, payload); + } + + @SneakyThrows + @Test + public void ping_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete("PONG"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals("PONG", payload); + } + + @SneakyThrows + @Test + public void ping_with_message_returns_success() { + // setup + String message = "RETURN OF THE PONG"; + String[] arguments = new String[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + String pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void ping_binary_with_message_returns_success() { + // setup + GlideString message = gs("RETURN OF THE PONG"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message); + GlideString pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void ping_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete("PONG"); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals("PONG", payload); + } + + @SneakyThrows + @Test + public void ping_with_message_with_route_returns_success() { + // setup + String message = "RETURN OF THE PONG"; + String[] arguments = new String[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + Route route = ALL_PRIMARIES; + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message, route); + String pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void ping_binary_with_message_with_route_returns_success() { + // setup + GlideString message = gs("RETURN OF THE PONG"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + Route route = ALL_PRIMARIES; + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Ping), eq(arguments), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ping(message, route); + GlideString pong = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, pong); + } + + @SneakyThrows + @Test + public void echo_returns_success() { + // setup + String message = "Valkey GLIDE"; + String[] arguments = new String[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Echo), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.echo(message); + String echo = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, echo); + } + + @SneakyThrows + @Test + public void echo_binary_returns_success() { + // setup + GlideString message = gs("Valkey GLIDE"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(message); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Echo), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.echo(message); + GlideString echo = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, echo); + } + + @SneakyThrows + @Test + public void echo_with_route_returns_success() { + // setup + String message = "Valkey GLIDE"; + String[] arguments = new String[] {message}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(message)); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Echo), eq(arguments), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.echo(message, RANDOM); + String echo = response.get().getSingleValue(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, echo); + } + + @SneakyThrows + @Test + public void echo_binary_with_route_returns_success() { + // setup + GlideString message = gs("Valkey GLIDE"); + GlideString[] arguments = new GlideString[] {message}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(message)); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Echo), eq(arguments), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.echo(message, RANDOM); + GlideString echo = response.get().getSingleValue(); + + // verify + assertEquals(testResponse, response); + assertEquals(message, echo); + } + + @SneakyThrows + @Test + public void info_returns_string() { + // setup + Map testPayload = new HashMap<>(); + testPayload.put("addr1", "value1"); + testPayload.put("addr2", "value2"); + testPayload.put("addr3", "value3"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.of(testPayload)); + when(commandManager.>submitNewCommand(eq(Info), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.info(); + + // verify + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasMultiData()); + Map payload = clusterValue.getMultiValue(); + assertEquals(testPayload, payload); + } + + @SneakyThrows + @Test + public void info_with_route_returns_string() { + // setup + Map testClusterValue = Map.of("addr1", "addr1 result", "addr2", "addr2 result"); + Route route = ALL_NODES; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.of(testClusterValue)); + when(commandManager.>submitNewCommand( + eq(Info), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.info(route); + + // verify + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasMultiData()); + Map clusterMap = clusterValue.getMultiValue(); + assertEquals("addr1 result", clusterMap.get("addr1")); + assertEquals("addr2 result", clusterMap.get("addr2")); + } + + @SneakyThrows + @Test + public void info_with_route_with_infoOptions_returns_string() { + // setup + String[] infoArguments = new String[] {"ALL", "DEFAULT"}; + Map testClusterValue = Map.of("addr1", "addr1 result", "addr2", "addr2 result"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.of(testClusterValue)); + + Route route = ALL_PRIMARIES; + when(commandManager.>submitNewCommand( + eq(Info), eq(infoArguments), eq(route), any())) + .thenReturn(testResponse); + + // exercise + InfoOptions options = + InfoOptions.builder() + .section(InfoOptions.Section.ALL) + .section(InfoOptions.Section.DEFAULT) + .build(); + CompletableFuture> response = service.info(options, route); + + // verify + assertEquals(testResponse.get(), response.get()); + ClusterValue clusterValue = response.get(); + assertTrue(clusterValue.hasMultiData()); + Map clusterMap = clusterValue.getMultiValue(); + assertEquals("addr1 result", clusterMap.get("addr1")); + assertEquals("addr2 result", clusterMap.get("addr2")); + } + + @Test + @SneakyThrows + public void info_with_single_node_route_returns_single_value() { + var commandManager = new TestCommandManager(null); + + var data = "info string"; + try (var client = new TestClient(commandManager, data)) { + var value = client.info(RANDOM).get(); + assertAll( + () -> assertTrue(value.hasSingleData()), + () -> assertEquals(data, value.getSingleValue())); + } + } + + @Test + @SneakyThrows + public void info_with_multi_node_route_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.info(ALL_NODES).get(); + assertAll( + () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); + } + } + + @Test + @SneakyThrows + public void info_with_options_and_single_node_route_returns_single_value() { + var commandManager = new TestCommandManager(null); + + var data = "info string"; + try (var client = new TestClient(commandManager, data)) { + var value = client.info(InfoOptions.builder().build(), RANDOM).get(); + assertAll( + () -> assertTrue(value.hasSingleData()), + () -> assertEquals(data, value.getSingleValue())); + } + } + + @Test + @SneakyThrows + public void info_with_options_and_multi_node_route_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("key1", "value1", "key2", "value2"); + try (var client = new TestClient(commandManager, data)) { + var value = client.info(InfoOptions.builder().build(), ALL_NODES).get(); + assertAll( + () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); + } + } + + @SneakyThrows + @Test + public void clientId_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(42L); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientId(); + + // verify + assertEquals(testResponse, response); + assertEquals(42L, response.get()); + } + + @Test + @SneakyThrows + public void clientId_with_multi_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("n1", 42L); + try (var client = new TestClient(commandManager, data)) { + var value = client.clientId(ALL_NODES).get(); + + assertEquals(data, value.getMultiValue()); + } + } + + @Test + @SneakyThrows + public void clientId_with_single_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + try (var client = new TestClient(commandManager, 42L)) { + var value = client.clientId(RANDOM).get(); + assertEquals(42, value.getSingleValue()); + } + } + + @SneakyThrows + @Test + public void clientGetName_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete("TEST"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientGetName(); + + // verify + assertEquals(testResponse, response); + assertEquals("TEST", response.get()); + } + + @Test + @SneakyThrows + public void clientGetName_with_single_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + try (var client = new TestClient(commandManager, "TEST")) { + var value = client.clientGetName(RANDOM).get(); + assertEquals("TEST", value.getSingleValue()); + } + } + + @Test + @SneakyThrows + public void clientGetName_with_multi_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("n1", "TEST"); + try (var client = new TestClient(commandManager, data)) { + var value = client.clientGetName(ALL_NODES).get(); + assertEquals(data, value.getMultiValue()); + } + } + + @SneakyThrows + @Test + public void configRewrite_without_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configRewrite_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigRewrite), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_without_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigResetStat), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + // TODO copy/move tests from GlideClientTest which call super for coverage + @SneakyThrows + @Test + public void configGet_returns_success() { + // setup + var testPayload = Map.of("timeout", "1000"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(testPayload); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(ConfigGet), eq(new String[] {"timeout"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.configGet(new String[] {"timeout"}); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(testPayload, payload); + } + + @Test + @SneakyThrows + // test checks that even a map returned as a single value when single node route is used + public void configGet_with_single_node_route_returns_single_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("timeout", "1000", "maxmemory", "1GB"); + try (var client = new TestClient(commandManager, data)) { + var value = client.configGet(TEST_ARGS, RANDOM).get(); + assertAll( + () -> assertTrue(value.hasSingleData()), + () -> assertEquals(data, value.getSingleValue())); + } + } + + @Test + @SneakyThrows + public void configGet_with_multi_node_route_returns_multi_value() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("node1", Map.of("timeout", "1000", "maxmemory", "1GB")); + try (var client = new TestClient(commandManager, data)) { + var value = client.configGet(TEST_ARGS, ALL_NODES).get(); + assertAll( + () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); + } + } + + @SneakyThrows + @Test + public void configSet_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigSet), eq(new String[] {"timeout", "1000"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configSet(Map.of("timeout", "1000")); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void configSet_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigSet), eq(new String[] {"value", "42"}), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configSet(Map.of("value", "42"), RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, response.get()); + } + + @SneakyThrows + @Test + public void time_returns_success() { + // setup + + String[] payload = new String[] {"UnixTime", "ms"}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(payload); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Time), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.time(); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get()); + } + + @SneakyThrows + @Test + public void time_returns_with_route_success() { + // setup + String[] payload = new String[] {"UnixTime", "ms"}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(payload)); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Time), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.time(RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(payload, response.get().getSingleValue()); + } + + @SneakyThrows + @Test + public void lastsave_returns_success() { + // setup + Long value = 42L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LastSave), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lastsave(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lastsave_returns_with_route_success() { + // setup + Long value = 42L; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(LastSave), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lastsave(RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get().getSingleValue()); + } + + @SneakyThrows + @Test + public void flushall_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FlushAll), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushall(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushall_with_mode_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushAll), eq(new String[] {SYNC.toString()}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushall(SYNC); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushall_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushAll), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushall(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushall_with_route_and_mode_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushAll), eq(new String[] {SYNC.toString()}), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushall(SYNC, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushdb_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FlushDB), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushdb(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushdb_with_mode_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushDB), eq(new String[] {SYNC.toString()}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushdb(SYNC); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushdb_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FlushDB), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushdb(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void flushdb_with_route_and_mode_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FlushDB), eq(new String[] {SYNC.toString()}), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.flushdb(SYNC, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void lolwut_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Lolwut), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {"1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Lolwut), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_returns_success() { + // setup + String value = "pewpew"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Lolwut), eq(new String[] {VERSION_VALKEY_API, "42"}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_params_returns_success() { + // setup + String value = "pewpew"; + String[] arguments = new String[] {VERSION_VALKEY_API, "42", "1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Lolwut), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lolwut(42, params); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Lolwut), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_params_and_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + String[] arguments = new String[] {"1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Lolwut), eq(arguments), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(params, RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Lolwut), eq(new String[] {VERSION_VALKEY_API, "42"}), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(42, RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void lolwut_with_version_and_params_and_route_returns_success() { + // setup + ClusterValue value = ClusterValue.ofSingleValue("pewpew"); + String[] arguments = new String[] {VERSION_VALKEY_API, "42", "1", "2"}; + int[] params = new int[] {1, 2}; + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(Lolwut), eq(arguments), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lolwut(42, params, RANDOM); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void dbsize_returns_success() { + // setup + Long value = 10L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(DBSize), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.dbsize(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void dbsize_with_route_returns_success() { + // setup + Long value = 10L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(DBSize), eq(new String[0]), eq(ALL_PRIMARIES), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.dbsize(ALL_PRIMARIES); + + // verify + assertEquals(testResponse, response); + assertEquals(value, response.get()); + } + + @SneakyThrows + @Test + public void functionLoad_returns_success() { + // setup + String code = "The best code ever"; + String[] args = new String[] {code}; + String value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, false); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_binary_returns_success() { + // setup + GlideString code = gs("The best code ever"); + GlideString[] args = new GlideString[] {code}; + GlideString value = gs("42"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, false); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_replace_returns_success() { + // setup + String code = "The best code ever"; + String[] args = new String[] {FunctionLoadOptions.REPLACE.toString(), code}; + String value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, true); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_replace_binary_returns_success() { + // setup + GlideString code = gs("The best code ever"); + GlideString[] args = new GlideString[] {gs(FunctionLoadOptions.REPLACE.toString()), code}; + GlideString value = gs("42"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, true); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_route_returns_success() { + // setup + String code = "The best code ever"; + String[] args = new String[] {code}; + String value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, false, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_route_binary_returns_success() { + // setup + GlideString code = gs("The best code ever"); + GlideString[] args = new GlideString[] {code}; + GlideString value = gs("42"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FunctionLoad), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, false, RANDOM); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_replace_with_route_returns_success() { + // setup + String code = "The best code ever"; + String[] args = new String[] {FunctionLoadOptions.REPLACE.toString(), code}; + String value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionLoad), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, true, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionLoad_with_replace_with_route_binary_returns_success() { + // setup + GlideString code = gs("The best code ever"); + GlideString[] args = new GlideString[] {gs(FunctionLoadOptions.REPLACE.toString()), code}; + GlideString value = gs("42"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FunctionLoad), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionLoad(code, true, RANDOM); + GlideString payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_returns_success() { + // setup + String[] args = new String[0]; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(false); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_binary_returns_success() { + // setup + GlideString[] args = new GlideString[0]; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionListBinary(false); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_with_pattern_returns_success() { + // setup + String pattern = "*"; + String[] args = new String[] {LIBRARY_NAME_VALKEY_API, pattern, WITH_CODE_VALKEY_API}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand(eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = service.functionList(pattern, true); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_binary_with_pattern_returns_success() { + // setup + GlideString pattern = gs("*"); + GlideString[] args = + new GlideString[] {gs(LIBRARY_NAME_VALKEY_API), pattern, gs(WITH_CODE_VALKEY_API)}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.[]>submitNewCommand( + eq(FunctionList), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]> response = + service.functionListBinary(pattern, true); + Map[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionList_with_route_returns_success() { + // setup + String[] args = new String[] {WITH_CODE_VALKEY_API}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]>> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.[]>>submitNewCommand( + eq(FunctionList), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]>> response = + service.functionList(true, RANDOM); + ClusterValue[]> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload.getSingleValue()); + } + + @SneakyThrows + @Test + public void functionList_binary_with_route_returns_success() { + // setup + GlideString[] args = new GlideString[] {gs(WITH_CODE_VALKEY_API)}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]>> testResponse = + new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.[]>>submitNewCommand( + eq(FunctionList), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]>> response = + service.functionListBinary(true, RANDOM); + ClusterValue[]> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload.getSingleValue()); + } + + @SneakyThrows + @Test + public void functionList_with_pattern_and_route_returns_success() { + // setup + String pattern = "*"; + String[] args = new String[] {LIBRARY_NAME_VALKEY_API, pattern}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]>> testResponse = new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.[]>>submitNewCommand( + eq(FunctionList), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]>> response = + service.functionList(pattern, false, RANDOM); + ClusterValue[]> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload.getSingleValue()); + } + + @SneakyThrows + @Test + public void functionList_binary_with_pattern_and_route_returns_success() { + // setup + GlideString pattern = gs("*"); + GlideString[] args = new GlideString[] {gs(LIBRARY_NAME_VALKEY_API), pattern}; + @SuppressWarnings("unchecked") + Map[] value = new Map[0]; + CompletableFuture[]>> testResponse = + new CompletableFuture<>(); + testResponse.complete(ClusterValue.ofSingleValue(value)); + + // match on protobuf request + when(commandManager.[]>>submitNewCommand( + eq(FunctionList), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture[]>> response = + service.functionListBinary(pattern, false, RANDOM); + ClusterValue[]> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload.getSingleValue()); + } + + @SneakyThrows + @Test + public void functionFlush_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_mode_returns_success() { + // setup + FlushMode mode = ASYNC; + String[] args = new String[] {mode.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(mode); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_route_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_mode_and_route_returns_success() { + // setup + FlushMode mode = ASYNC; + String[] args = new String[] {mode.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(mode, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionDelete_returns_success() { + // setup + String libName = "GLIDE"; + String[] args = new String[] {libName}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionDelete_binary_returns_success() { + // setup + GlideString libName = gs("GLIDE"); + GlideString[] args = new GlideString[] {libName}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionDelete_with_route_returns_success() { + // setup + String libName = "GLIDE"; + String[] args = new String[] {libName}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionDelete_with_route_binary_returns_success() { + // setup + GlideString libName = gs("GLIDE"); + GlideString[] args = new GlideString[] {libName}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void unwatch_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(UnWatch), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.unwatch(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void unwatch_with_route_returns_success() { + // setup + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(UnWatch), eq(new String[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.unwatch(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void fcall_without_keys_and_without_args_returns_success() { + // setup + String function = "func"; + String[] args = new String[] {function, "0"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_binary_without_keys_and_without_args_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_without_keys_and_without_args_but_with_route_returns_success() { + // setup + String function = "func"; + String[] args = new String[] {function, "0"}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCall), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcall(function, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_binary_without_keys_and_without_args_but_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCall), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcall(function, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_without_keys_returns_success() { + // setup + String function = "func"; + String[] arguments = new String[] {"1", "2"}; + String[] args = new String[] {function, "0", "1", "2"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_binary_without_keys_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCall), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcall(function, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_without_keys_and_with_route_returns_success() { + // setup + String function = "func"; + String[] arguments = new String[] {"1", "2"}; + String[] args = new String[] {function, "0", "1", "2"}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCall), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcall(function, arguments, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcall_bianry_without_keys_and_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCall), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcall(function, arguments, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_without_keys_and_without_args_returns_success() { + // setup + String function = "func"; + String[] args = new String[] {function, "0"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_and_without_args_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionKill_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionKill), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionKill(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionKill_with_route_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionKill), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionKill(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionStats_returns_success() { + // setup + String[] args = new String[0]; + ClusterValue>> value = + ClusterValue.ofSingleValue(Map.of("1", Map.of("2", 2))); + CompletableFuture>>> testResponse = + new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>>>submitNewCommand( + eq(FunctionStats), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>>> response = + service.functionStats(); + ClusterValue>> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionStatsBinary_returns_success() { + // setup + GlideString[] args = new GlideString[0]; + ClusterValue>> value = + ClusterValue.ofSingleValue(Map.of(gs("1"), Map.of(gs("2"), 2))); + CompletableFuture>>> testResponse = + new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>>>submitNewCommand( + eq(FunctionStats), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>>> response = + service.functionStatsBinary(); + ClusterValue>> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_without_keys_and_without_args_but_with_route_returns_success() { + // setup + String function = "func"; + String[] args = new String[] {function, "0"}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCallReadOnly), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcallReadOnly(function, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_and_without_args_but_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] args = new GlideString[] {function, gs("0")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCallReadOnly), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.fcallReadOnly(function, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_without_keys_returns_success() { + // setup + String function = "func"; + String[] arguments = new String[] {"1", "2"}; + String[] args = new String[] {function, "0", "1", "2"}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + Object value = "42"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FCallReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.fcallReadOnly(function, arguments); + Object payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_without_keys_and_with_route_returns_success() { + // setup + String function = "func"; + String[] arguments = new String[] {"1", "2"}; + String[] args = new String[] {function, "0", "1", "2"}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCallReadOnly), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.fcallReadOnly(function, arguments, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void fcallReadOnly_binary_without_keys_and_with_route_returns_success() { + // setup + GlideString function = gs("func"); + GlideString[] arguments = new GlideString[] {gs("1"), gs("2")}; + GlideString[] args = new GlideString[] {function, gs("0"), gs("1"), gs("2")}; + ClusterValue value = ClusterValue.ofSingleValue("42"); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FCallReadOnly), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = + service.fcallReadOnly(function, arguments, RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionStats_with_route_returns_success() { + // setup + String[] args = new String[0]; + ClusterValue>> value = + ClusterValue.ofSingleValue(Map.of("1", Map.of("2", 2))); + CompletableFuture>>> testResponse = + new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>>>submitNewCommand( + eq(FunctionStats), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>>> response = + service.functionStats(RANDOM); + ClusterValue>> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionStatsBinary_with_route_returns_success() { + // setup + GlideString[] args = new GlideString[0]; + ClusterValue>> value = + ClusterValue.ofSingleValue(Map.of(gs("1"), Map.of(gs("2"), 2))); + CompletableFuture>>> testResponse = + new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>>>submitNewCommand( + eq(FunctionStats), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture>>> response = + service.functionStatsBinary(RANDOM); + ClusterValue>> payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionDump_returns_success() { + // setup + byte[] value = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDump), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDump(); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionDump_with_route_returns_success() { + // setup + ClusterValue value = ClusterValue.of(new byte[] {42}); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FunctionDump), eq(new GlideString[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.functionDump(RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionRestore_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, FunctionRestorePolicy.FLUSH); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_route_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_and_route_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.functionRestore(data, FunctionRestorePolicy.FLUSH, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void randomKey_with_route() { + // setup + String key1 = "key1"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(key1); + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(RandomKey), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + CompletableFuture response = service.randomKey(route); + + // verify + assertEquals(testResponse, response); + } + + @SneakyThrows + @Test + public void randomKey() { + // setup + String key1 = "key1"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(key1); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RandomKey), eq(new String[0]), any())) + .thenReturn(testResponse); + CompletableFuture response = service.randomKey(); + + // verify + assertEquals(testResponse, response); + } + + @SneakyThrows + @Test + public void spublish_returns_success() { + // setup + String channel = "channel"; + String message = "message"; + String[] arguments = new String[] {channel, message}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SPublish), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.publish(message, channel, true); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void sort_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sort(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_binary_returns_success() { + // setup + GlideString[] result = new GlideString[] {gs("1"), gs("2"), gs("3")}; + GlideString key = gs("key"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Sort), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sort(key); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_options_binary_returns_success() { + // setup + GlideString[] result = new GlideString[] {gs("1"), gs("2"), gs("3")}; + GlideString key = gs("key"); + Long limitOffset = 0L; + Long limitCount = 2L; + GlideString[] args = + new GlideString[] { + key, + gs(LIMIT_COMMAND_STRING), + gs(limitOffset.toString()), + gs(limitCount.toString()), + gs(DESC.toString()), + gs(ALPHA_COMMAND_STRING) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortReadOnly(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_binary_returns_success() { + // setup + GlideString[] result = new GlideString[] {gs("1"), gs("2"), gs("3")}; + GlideString key = gs("key"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(SortReadOnly), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortReadOnly(key); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_binary_returns_success() { + // setup + GlideString[] result = new GlideString[] {gs("1"), gs("2"), gs("3")}; + GlideString key = gs("key"); + Long limitOffset = 0L; + Long limitCount = 2L; + GlideString[] args = + new GlideString[] { + key, + gs(LIMIT_COMMAND_STRING), + gs(limitOffset.toString()), + gs(limitCount.toString()), + gs(DESC.toString()), + gs(ALPHA_COMMAND_STRING) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + GlideString[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Sort), eq(new String[] {key, STORE_COMMAND_STRING, destKey}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortStore(key, destKey); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_binary_returns_success() { + // setup + Long result = 5L; + GlideString key = gs("key"); + GlideString destKey = gs("destKey"); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Sort), eq(new GlideString[] {key, gs(STORE_COMMAND_STRING), destKey}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortStore(key, destKey); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_binary_returns_success() { + // setup + Long result = 5L; + GlideString key = gs("key"); + GlideString destKey = gs("destKey"); + Long limitOffset = 0L; + Long limitCount = 2L; + GlideString[] args = + new GlideString[] { + key, + gs(LIMIT_COMMAND_STRING), + gs(limitOffset.toString()), + gs(limitCount.toString()), + gs(DESC.toString()), + gs(ALPHA_COMMAND_STRING), + gs(STORE_COMMAND_STRING), + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void scan_new_cursor() { + CommandManager.ClusterScanCursorDetail mockCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockCursor.getCursorHandle()).thenReturn("1"); + + final Object[] result = new Object[] {mockCursor.getCursorHandle(), new Object[] {"foo"}}; + final CompletableFuture testResponse = CompletableFuture.completedFuture(result); + when(commandManager.submitClusterScan( + eq(ClusterScanCursor.INITIAL_CURSOR_INSTANCE), + eq(ScanOptions.builder().build()), + any())) + .thenReturn(testResponse); + + final CompletableFuture actualResponse = + service.scan(ClusterScanCursor.initalCursor()); + assertEquals( + mockCursor.getCursorHandle(), + ((CommandManager.ClusterScanCursorDetail) actualResponse.get()[0]).getCursorHandle()); + } + + @SneakyThrows + @Test + public void scan_existing_cursor() { + CommandManager.ClusterScanCursorDetail mockCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockCursor.getCursorHandle()).thenReturn("1"); + + CommandManager.ClusterScanCursorDetail mockResultCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockResultCursor.getCursorHandle()).thenReturn("2"); + + final Object[] result = new Object[] {mockResultCursor.getCursorHandle(), new Object[] {"foo"}}; + final CompletableFuture testResponse = CompletableFuture.completedFuture(result); + when(commandManager.submitClusterScan( + eq(mockCursor), eq(ScanOptions.builder().build()), any())) + .thenReturn(testResponse); + + CompletableFuture actualResponse = service.scan(mockCursor); + assertEquals( + mockResultCursor.getCursorHandle(), + ((CommandManager.ClusterScanCursorDetail) actualResponse.get()[0]).getCursorHandle()); + } + + @SneakyThrows + @Test + public void scan_binary_existing_cursor() { + CommandManager.ClusterScanCursorDetail mockCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockCursor.getCursorHandle()).thenReturn("1"); + + CommandManager.ClusterScanCursorDetail mockResultCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockResultCursor.getCursorHandle()).thenReturn("2"); + + final Object[] result = + new Object[] {mockResultCursor.getCursorHandle(), new Object[] {gs("foo")}}; + final CompletableFuture testResponse = CompletableFuture.completedFuture(result); + when(commandManager.submitClusterScan( + eq(mockCursor), eq(ScanOptions.builder().build()), any())) + .thenReturn(testResponse); + + CompletableFuture actualResponse = service.scan(mockCursor); + Object[] payload = actualResponse.get(); + assertEquals( + mockResultCursor.getCursorHandle(), + ((CommandManager.ClusterScanCursorDetail) payload[0]).getCursorHandle()); + assertArrayEquals(new Object[] {gs("foo")}, (Object[]) payload[1]); + } + + @SneakyThrows + @Test + public void scan_new_cursor_options() { + CommandManager.ClusterScanCursorDetail mockCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockCursor.getCursorHandle()).thenReturn("1"); + + final Object[] result = new Object[] {mockCursor.getCursorHandle(), new Object[] {"foo"}}; + final CompletableFuture testResponse = CompletableFuture.completedFuture(result); + when(commandManager.submitClusterScan( + eq(ClusterScanCursor.INITIAL_CURSOR_INSTANCE), + eq( + ScanOptions.builder() + .matchPattern("key:*") + .count(10L) + .type(ScanOptions.ObjectType.STRING) + .build()), + any())) + .thenReturn(testResponse); + + final CompletableFuture actualResponse = + service.scan( + ClusterScanCursor.initalCursor(), + ScanOptions.builder() + .matchPattern("key:*") + .count(10L) + .type(ScanOptions.ObjectType.STRING) + .build()); + + assertEquals( + mockCursor.getCursorHandle(), + ((CommandManager.ClusterScanCursorDetail) actualResponse.get()[0]).getCursorHandle()); + } + + @SneakyThrows + @Test + public void scan_existing_cursor_options() { + CommandManager.ClusterScanCursorDetail mockCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockCursor.getCursorHandle()).thenReturn("1"); + + CommandManager.ClusterScanCursorDetail mockResultCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockResultCursor.getCursorHandle()).thenReturn("2"); + + final Object[] result = new Object[] {mockResultCursor.getCursorHandle(), new Object[] {"foo"}}; + final CompletableFuture testResponse = CompletableFuture.completedFuture(result); + when(commandManager.submitClusterScan( + eq(mockCursor), + eq( + ScanOptions.builder() + .matchPattern("key:*") + .count(10L) + .type(ScanOptions.ObjectType.STRING) + .build()), + any())) + .thenReturn(testResponse); + + CompletableFuture actualResponse = + service.scan( + mockCursor, + ScanOptions.builder() + .matchPattern("key:*") + .count(10L) + .type(ScanOptions.ObjectType.STRING) + .build()); + assertEquals( + mockResultCursor.getCursorHandle(), + ((CommandManager.ClusterScanCursorDetail) actualResponse.get()[0]).getCursorHandle()); + } + + @SneakyThrows + @Test + public void scan_binary_existing_cursor_options() { + CommandManager.ClusterScanCursorDetail mockCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockCursor.getCursorHandle()).thenReturn("1"); + + CommandManager.ClusterScanCursorDetail mockResultCursor = + Mockito.mock(CommandManager.ClusterScanCursorDetail.class); + when(mockResultCursor.getCursorHandle()).thenReturn("2"); + + final Object[] result = + new Object[] {mockResultCursor.getCursorHandle(), new Object[] {gs("foo")}}; + final CompletableFuture testResponse = CompletableFuture.completedFuture(result); + when(commandManager.submitClusterScan( + eq(mockCursor), + eq( + ScanOptions.builder() + .matchPattern("key:*") + .count(10L) + .type(ScanOptions.ObjectType.STRING) + .build()), + any())) + .thenReturn(testResponse); + + CompletableFuture actualResponse = + service.scan( + mockCursor, + ScanOptions.builder() + .matchPattern("key:*") + .count(10L) + .type(ScanOptions.ObjectType.STRING) + .build()); + Object[] payload = actualResponse.get(); + assertEquals( + mockResultCursor.getCursorHandle(), + ((CommandManager.ClusterScanCursorDetail) payload[0]).getCursorHandle()); + assertArrayEquals(new Object[] {gs("foo")}, (Object[]) payload[1]); + } +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java deleted file mode 100644 index 054d6b7ded..0000000000 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ /dev/null @@ -1,3262 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api; - -import static glide.api.BaseClient.OK; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; -import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; -import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; -import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; -import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; -import static glide.api.models.commands.StreamAddOptions.NO_MAKE_STREAM_REDIS_API; -import static glide.api.models.commands.StreamAddOptions.TRIM_EXACT_REDIS_API; -import static glide.api.models.commands.StreamAddOptions.TRIM_LIMIT_REDIS_API; -import static glide.api.models.commands.StreamAddOptions.TRIM_MAXLEN_REDIS_API; -import static glide.api.models.commands.StreamAddOptions.TRIM_MINID_REDIS_API; -import static glide.api.models.commands.StreamAddOptions.TRIM_NOT_EXACT_REDIS_API; -import static glide.utils.ArrayTransformUtils.concatenateArrays; -import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; -import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static redis_request.RedisRequestOuterClass.RequestType.Blpop; -import static redis_request.RedisRequestOuterClass.RequestType.Brpop; -import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; -import static redis_request.RedisRequestOuterClass.RequestType.ClientId; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigGet; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; -import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; -import static redis_request.RedisRequestOuterClass.RequestType.Decr; -import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; -import static redis_request.RedisRequestOuterClass.RequestType.Del; -import static redis_request.RedisRequestOuterClass.RequestType.Echo; -import static redis_request.RedisRequestOuterClass.RequestType.Exists; -import static redis_request.RedisRequestOuterClass.RequestType.Expire; -import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.GetRange; -import static redis_request.RedisRequestOuterClass.RequestType.GetString; -import static redis_request.RedisRequestOuterClass.RequestType.HLen; -import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; -import static redis_request.RedisRequestOuterClass.RequestType.HashDel; -import static redis_request.RedisRequestOuterClass.RequestType.HashExists; -import static redis_request.RedisRequestOuterClass.RequestType.HashGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashGetAll; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.HashMGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashSet; -import static redis_request.RedisRequestOuterClass.RequestType.Hvals; -import static redis_request.RedisRequestOuterClass.RequestType.Incr; -import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.Info; -import static redis_request.RedisRequestOuterClass.RequestType.LInsert; -import static redis_request.RedisRequestOuterClass.RequestType.LLen; -import static redis_request.RedisRequestOuterClass.RequestType.LPop; -import static redis_request.RedisRequestOuterClass.RequestType.LPush; -import static redis_request.RedisRequestOuterClass.RequestType.LPushX; -import static redis_request.RedisRequestOuterClass.RequestType.LRange; -import static redis_request.RedisRequestOuterClass.RequestType.LRem; -import static redis_request.RedisRequestOuterClass.RequestType.LTrim; -import static redis_request.RedisRequestOuterClass.RequestType.LastSave; -import static redis_request.RedisRequestOuterClass.RequestType.Lindex; -import static redis_request.RedisRequestOuterClass.RequestType.MGet; -import static redis_request.RedisRequestOuterClass.RequestType.MSet; -import static redis_request.RedisRequestOuterClass.RequestType.PExpire; -import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.PTTL; -import static redis_request.RedisRequestOuterClass.RequestType.Persist; -import static redis_request.RedisRequestOuterClass.RequestType.PfAdd; -import static redis_request.RedisRequestOuterClass.RequestType.PfCount; -import static redis_request.RedisRequestOuterClass.RequestType.PfMerge; -import static redis_request.RedisRequestOuterClass.RequestType.Ping; -import static redis_request.RedisRequestOuterClass.RequestType.RPop; -import static redis_request.RedisRequestOuterClass.RequestType.RPush; -import static redis_request.RedisRequestOuterClass.RequestType.RPushX; -import static redis_request.RedisRequestOuterClass.RequestType.SAdd; -import static redis_request.RedisRequestOuterClass.RequestType.SCard; -import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.SInter; -import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; -import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMembers; -import static redis_request.RedisRequestOuterClass.RequestType.SMove; -import static redis_request.RedisRequestOuterClass.RequestType.SRem; -import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; -import static redis_request.RedisRequestOuterClass.RequestType.Select; -import static redis_request.RedisRequestOuterClass.RequestType.SetRange; -import static redis_request.RedisRequestOuterClass.RequestType.SetString; -import static redis_request.RedisRequestOuterClass.RequestType.Strlen; -import static redis_request.RedisRequestOuterClass.RequestType.TTL; -import static redis_request.RedisRequestOuterClass.RequestType.Time; -import static redis_request.RedisRequestOuterClass.RequestType.Type; -import static redis_request.RedisRequestOuterClass.RequestType.Unlink; -import static redis_request.RedisRequestOuterClass.RequestType.XAdd; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount; -import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; -import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZScore; -import static redis_request.RedisRequestOuterClass.RequestType.Zadd; -import static redis_request.RedisRequestOuterClass.RequestType.Zcard; -import static redis_request.RedisRequestOuterClass.RequestType.Zcount; -import static redis_request.RedisRequestOuterClass.RequestType.Zrange; -import static redis_request.RedisRequestOuterClass.RequestType.Zrank; -import static redis_request.RedisRequestOuterClass.RequestType.Zrem; - -import glide.api.models.Script; -import glide.api.models.Transaction; -import glide.api.models.commands.ExpireOptions; -import glide.api.models.commands.InfoOptions; -import glide.api.models.commands.RangeOptions; -import glide.api.models.commands.RangeOptions.InfLexBound; -import glide.api.models.commands.RangeOptions.InfScoreBound; -import glide.api.models.commands.RangeOptions.LexBoundary; -import glide.api.models.commands.RangeOptions.RangeByIndex; -import glide.api.models.commands.RangeOptions.RangeByLex; -import glide.api.models.commands.RangeOptions.RangeByScore; -import glide.api.models.commands.RangeOptions.ScoreBoundary; -import glide.api.models.commands.ScriptOptions; -import glide.api.models.commands.SetOptions; -import glide.api.models.commands.SetOptions.Expiry; -import glide.api.models.commands.StreamAddOptions; -import glide.api.models.commands.ZaddOptions; -import glide.managers.CommandManager; -import glide.managers.ConnectionManager; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import lombok.SneakyThrows; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -public class RedisClientTest { - - RedisClient service; - - ConnectionManager connectionManager; - - CommandManager commandManager; - - @BeforeEach - public void setUp() { - connectionManager = mock(ConnectionManager.class); - commandManager = mock(CommandManager.class); - service = new RedisClient(connectionManager, commandManager); - } - - @SneakyThrows - @Test - public void customCommand_returns_success() { - // setup - String key = "testKey"; - Object value = "testValue"; - String cmd = "GETSTRING"; - String[] arguments = new String[] {cmd, key}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(CustomCommand), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.customCommand(arguments); - String payload = (String) response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void exec() { - // setup - Object[] value = new Object[] {"PONG", "PONG"}; - Transaction transaction = new Transaction().ping().ping(); - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(transaction), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.exec(transaction); - Object[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertArrayEquals(value, payload); - } - - @SneakyThrows - @Test - public void echo_returns_success() { - // setup - String message = "GLIDE FOR REDIS"; - String[] arguments = new String[] {message}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(message); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Echo), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.echo(message); - String echo = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(message, echo); - } - - @SneakyThrows - @Test - public void ping_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete("PONG"); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ping(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals("PONG", payload); - } - - @SneakyThrows - @Test - public void ping_with_message_returns_success() { - // setup - String message = "RETURN OF THE PONG"; - String[] arguments = new String[] {message}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(message); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ping(message); - String pong = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(message, pong); - } - - @SneakyThrows - @Test - public void select_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - long index = 5L; - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(Select), eq(new String[] {Long.toString(index)}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.select(index); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void del_returns_long_success() { - // setup - String[] keys = new String[] {"testKey1", "testKey2"}; - Long numberDeleted = 1L; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(numberDeleted); - when(commandManager.submitNewCommand(eq(Del), eq(keys), any())).thenReturn(testResponse); - - // exercise - CompletableFuture response = service.del(keys); - Long result = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(numberDeleted, result); - } - - @SneakyThrows - @Test - public void unlink_returns_long_success() { - // setup - String[] keys = new String[] {"testKey1", "testKey2"}; - Long numberUnlinked = 1L; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(numberUnlinked); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Unlink), eq(keys), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.unlink(keys); - Long result = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(numberUnlinked, result); - } - - @SneakyThrows - @Test - public void get_returns_success() { - // setup - String key = "testKey"; - String value = "testValue"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - when(commandManager.submitNewCommand(eq(GetString), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.get(key); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void set_returns_success() { - // setup - String key = "testKey"; - String value = "testValue"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(null); - when(commandManager.submitNewCommand( - eq(SetString), eq(new String[] {key, value}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.set(key, value); - Object okResponse = response.get(); - - // verify - assertEquals(testResponse, response); - assertNull(okResponse); - } - - @SneakyThrows - @Test - public void set_with_SetOptions_OnlyIfExists_returns_success() { - // setup - String key = "testKey"; - String value = "testValue"; - SetOptions setOptions = - SetOptions.builder() - .conditionalSet(ONLY_IF_EXISTS) - .returnOldValue(false) - .expiry(Expiry.KeepExisting()) - .build(); - String[] arguments = new String[] {key, value, ONLY_IF_EXISTS.getRedisApi(), "KEEPTTL"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(null); - when(commandManager.submitNewCommand(eq(SetString), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.set(key, value, setOptions); - - // verify - assertEquals(testResponse, response); - assertNull(response.get()); - } - - @SneakyThrows - @Test - public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { - // setup - String key = "testKey"; - String value = "testValue"; - SetOptions setOptions = - SetOptions.builder() - .conditionalSet(ONLY_IF_DOES_NOT_EXIST) - .returnOldValue(true) - .expiry(Expiry.UnixSeconds(60L)) - .build(); - String[] arguments = - new String[] { - key, value, ONLY_IF_DOES_NOT_EXIST.getRedisApi(), RETURN_OLD_VALUE, "EXAT", "60" - }; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - when(commandManager.submitNewCommand(eq(SetString), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.set(key, value, setOptions); - - // verify - assertNotNull(response); - assertEquals(value, response.get()); - } - - @SneakyThrows - @Test - public void exists_returns_long_success() { - // setup - String[] keys = new String[] {"testKey1", "testKey2"}; - Long numberExisting = 1L; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(numberExisting); - when(commandManager.submitNewCommand(eq(Exists), eq(keys), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.exists(keys); - Long result = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(numberExisting, result); - } - - @SneakyThrows - @Test - public void expire_returns_success() { - // setup - String key = "testKey"; - long seconds = 10L; - String[] arguments = new String[] {key, Long.toString(seconds)}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.TRUE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.expire(key, seconds); - - // verify - assertEquals(testResponse, response); - assertEquals(true, response.get()); - } - - @SneakyThrows - @Test - public void expire_with_expireOptions_returns_success() { - // setup - String key = "testKey"; - long seconds = 10L; - String[] arguments = new String[] {key, Long.toString(seconds), "NX"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.FALSE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.expire(key, seconds, ExpireOptions.HAS_NO_EXPIRY); - - // verify - assertEquals(testResponse, response); - assertEquals(false, response.get()); - } - - @SneakyThrows - @Test - public void expireAt_returns_success() { - // setup - String key = "testKey"; - long unixSeconds = 100000L; - String[] arguments = new String[] {key, Long.toString(unixSeconds)}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.TRUE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.expireAt(key, unixSeconds); - - // verify - assertEquals(testResponse, response); - assertEquals(true, response.get()); - } - - @SneakyThrows - @Test - public void expireAt_with_expireOptions_returns_success() { - // setup - String key = "testKey"; - long unixSeconds = 100000L; - String[] arguments = new String[] {key, Long.toString(unixSeconds), "XX"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.FALSE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.expireAt(key, unixSeconds, ExpireOptions.HAS_EXISTING_EXPIRY); - - // verify - assertEquals(testResponse, response); - assertEquals(false, response.get()); - } - - @SneakyThrows - @Test - public void pexpire_returns_success() { - // setup - String key = "testKey"; - long milliseconds = 50000L; - String[] arguments = new String[] {key, Long.toString(milliseconds)}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.TRUE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.pexpire(key, milliseconds); - - // verify - assertEquals(testResponse, response); - assertEquals(true, response.get()); - } - - @SneakyThrows - @Test - public void pexpire_with_expireOptions_returns_success() { - // setup - String key = "testKey"; - long milliseconds = 50000L; - String[] arguments = new String[] {key, Long.toString(milliseconds), "LT"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.FALSE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.pexpire(key, milliseconds, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); - - // verify - assertEquals(testResponse, response); - assertEquals(false, response.get()); - } - - @SneakyThrows - @Test - public void pexpireAt_returns_success() { - // setup - String key = "testKey"; - long unixMilliseconds = 999999L; - String[] arguments = new String[] {key, Long.toString(unixMilliseconds)}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.TRUE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.pexpireAt(key, unixMilliseconds); - - // verify - assertEquals(testResponse, response); - assertEquals(true, response.get()); - } - - @SneakyThrows - @Test - public void pexpireAt_with_expireOptions_returns_success() { - // setup - String key = "testKey"; - long unixMilliseconds = 999999L; - String[] arguments = new String[] {key, Long.toString(unixMilliseconds), "GT"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.FALSE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.pexpireAt(key, unixMilliseconds, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); - - // verify - assertEquals(testResponse, response); - assertEquals(false, response.get()); - } - - @SneakyThrows - @Test - public void ttl_returns_success() { - // setup - String key = "testKey"; - long ttl = 999L; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(ttl); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(TTL), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ttl(key); - - // verify - assertEquals(testResponse, response); - assertEquals(ttl, response.get()); - } - - @SneakyThrows - @Test - public void invokeScript_returns_success() { - // setup - Script script = mock(Script.class); - String hash = UUID.randomUUID().toString(); - when(script.getHash()).thenReturn(hash); - String payload = "hello"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(payload); - - // match on protobuf request - when(commandManager.submitScript(eq(script), eq(List.of()), eq(List.of()), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.invokeScript(script); - - // verify - assertEquals(testResponse, response); - assertEquals(payload, response.get()); - } - - @SneakyThrows - @Test - public void invokeScript_with_ScriptOptions_returns_success() { - // setup - Script script = mock(Script.class); - String hash = UUID.randomUUID().toString(); - when(script.getHash()).thenReturn(hash); - String payload = "hello"; - - ScriptOptions options = - ScriptOptions.builder().key("key1").key("key2").arg("arg1").arg("arg2").build(); - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(payload); - - // match on protobuf request - when(commandManager.submitScript( - eq(script), eq(List.of("key1", "key2")), eq(List.of("arg1", "arg2")), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.invokeScript(script, options); - - // verify - assertEquals(testResponse, response); - assertEquals(payload, response.get()); - } - - @SneakyThrows - @Test - public void pttl_returns_success() { - // setup - String key = "testKey"; - long pttl = 999000L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(pttl); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PTTL), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.pttl(key); - - // verify - assertEquals(testResponse, response); - assertEquals(pttl, response.get()); - } - - @SneakyThrows - @Test - public void persist_returns_success() { - // setup - String key = "testKey"; - Boolean isTimeoutRemoved = true; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(isTimeoutRemoved); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Persist), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.persist(key); - - // verify - assertEquals(testResponse, response); - assertEquals(isTimeoutRemoved, response.get()); - } - - @SneakyThrows - @Test - public void info_returns_success() { - // setup - String testPayload = "Key: Value"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(testPayload); - when(commandManager.submitNewCommand(eq(Info), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.info(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(testPayload, payload); - } - - @SneakyThrows - @Test - public void info_with_multiple_InfoOptions_returns_success() { - // setup - String[] arguments = - new String[] {InfoOptions.Section.ALL.toString(), InfoOptions.Section.DEFAULT.toString()}; - String testPayload = "Key: Value"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(testPayload); - when(commandManager.submitNewCommand(eq(Info), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - InfoOptions options = - InfoOptions.builder() - .section(InfoOptions.Section.ALL) - .section(InfoOptions.Section.DEFAULT) - .build(); - CompletableFuture response = service.info(options); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(testPayload, payload); - } - - @SneakyThrows - @Test - public void info_with_empty_InfoOptions_returns_success() { - // setup - String testPayload = "Key: Value"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(testPayload); - when(commandManager.submitNewCommand(eq(Info), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.info(InfoOptions.builder().build()); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(testPayload, payload); - } - - @SneakyThrows - @Test - public void mget_returns_success() { - // setup - String[] keys = {"key1", null, "key2"}; - String[] values = {"value1", null, "value2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(values); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(MGet), eq(keys), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.mget(keys); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(values, payload); - } - - @SneakyThrows - @Test - public void mset_returns_success() { - // setup - Map keyValueMap = new LinkedHashMap<>(); - keyValueMap.put("key1", "value1"); - keyValueMap.put("key2", "value2"); - String[] args = {"key1", "value1", "key2", "value2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(MSet), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.mset(keyValueMap); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void incr_returns_success() { - // setup - String key = "testKey"; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Incr), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.incr(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void incrBy_returns_success() { - // setup - String key = "testKey"; - long amount = 1L; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(IncrBy), eq(new String[] {key, Long.toString(amount)}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.incrBy(key, amount); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void incrByFloat_returns_success() { - // setup - String key = "testKey"; - double amount = 1.1; - Double value = 10.1; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(IncrByFloat), eq(new String[] {key, Double.toString(amount)}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.incrByFloat(key, amount); - Double payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void decr_returns_success() { - // setup - String key = "testKey"; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Decr), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.decr(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void decrBy_returns_success() { - // setup - String key = "testKey"; - long amount = 1L; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(DecrBy), eq(new String[] {key, Long.toString(amount)}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.decrBy(key, amount); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void strlen_returns_success() { - // setup - String key = "testKey"; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Strlen), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.strlen(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void setrange_returns_success() { - // setup - String key = "testKey"; - int offset = 42; - String str = "pewpew"; - String[] arguments = new String[] {key, Integer.toString(offset), str}; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SetRange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.setrange(key, offset, str); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void getrange_returns_success() { - // setup - String key = "testKey"; - int start = 42; - int end = 54; - String[] arguments = new String[] {key, Integer.toString(start), Integer.toString(end)}; - String value = "pewpew"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(GetRange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.getrange(key, start, end); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hget_success() { - // setup - String key = "testKey"; - String field = "field"; - String[] args = new String[] {key, field}; - String value = "value"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - when(commandManager.submitNewCommand(eq(HashGet), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hget(key, field); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hset_success() { - // setup - String key = "testKey"; - Map fieldValueMap = new LinkedHashMap<>(); - fieldValueMap.put("field1", "value1"); - fieldValueMap.put("field2", "value2"); - String[] args = new String[] {key, "field1", "value1", "field2", "value2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - when(commandManager.submitNewCommand(eq(HashSet), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hset(key, fieldValueMap); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hsetnx_success() { - // setup - String key = "testKey"; - String field = "testField"; - String value = "testValue"; - String[] args = new String[] {key, field, value}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(Boolean.TRUE); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(HSetNX), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hsetnx(key, field, value); - Boolean payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertTrue(payload); - } - - @SneakyThrows - @Test - public void hdel_success() { - // setup - String key = "testKey"; - String[] fields = {"testField1", "testField2"}; - String[] args = {key, "testField1", "testField2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(HashDel), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hdel(key, fields); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hlen_success() { - // setup - String key = "testKey"; - String[] args = {key}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(HLen), eq(args), any())).thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hlen(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hvals_success() { - // setup - String key = "testKey"; - String[] args = {key}; - String[] values = new String[] {"value1", "value2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(values); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Hvals), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hvals(key); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(values, payload); - } - - @SneakyThrows - @Test - public void hmget_success() { - // setup - String key = "testKey"; - String[] fields = {"testField1", "testField2"}; - String[] args = {"testKey", "testField1", "testField2"}; - String[] value = {"testValue1", "testValue2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(HashMGet), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hmget(key, fields); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hexists_success() { - // setup - String key = "testKey"; - String field = "testField"; - String[] args = new String[] {key, field}; - Boolean value = true; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(HashExists), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hexists(key, field); - Boolean payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hgetall_success() { - // setup - String key = "testKey"; - String[] args = new String[] {key}; - Map value = new LinkedHashMap<>(); - value.put("key1", "field1"); - value.put("key2", "field2"); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(HashGetAll), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.hgetall(key); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hincrBy_returns_success() { - // setup - String key = "testKey"; - String field = "field"; - long amount = 1L; - Long value = 10L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(HashIncrBy), eq(new String[] {key, field, Long.toString(amount)}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hincrBy(key, field, amount); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void hincrByFloat_returns_success() { - // setup - String key = "testKey"; - String field = "field"; - double amount = 1.0; - Double value = 10.0; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(HashIncrByFloat), eq(new String[] {key, field, Double.toString(amount)}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.hincrByFloat(key, field, amount); - Double payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lpush_returns_success() { - // setup - String key = "testKey"; - String[] elements = new String[] {"value1", "value2"}; - String[] args = new String[] {key, "value1", "value2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LPush), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lpush(key, elements); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lpop_returns_success() { - // setup - String key = "testKey"; - String[] args = new String[] {key}; - String value = "value"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LPop), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lpop(key); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lpopCount_returns_success() { - // setup - String key = "testKey"; - long count = 2L; - String[] args = new String[] {key, Long.toString(count)}; - String[] value = new String[] {"value1", "value2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LPop), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lpopCount(key, count); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lrange_returns_success() { - // setup - String key = "testKey"; - long start = 2L; - long end = 4L; - String[] args = new String[] {key, Long.toString(start), Long.toString(end)}; - String[] value = new String[] {"value1", "value2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LRange), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lrange(key, start, end); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lindex_returns_success() { - // setup - String key = "testKey"; - long index = 2; - String[] args = new String[] {key, Long.toString(index)}; - String value = "value"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Lindex), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lindex(key, index); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void ltrim_returns_success() { - // setup - String key = "testKey"; - long start = 2L; - long end = 2L; - String[] args = new String[] {key, Long.toString(end), Long.toString(start)}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LTrim), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ltrim(key, start, end); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void llen_returns_success() { - // setup - String key = "testKey"; - String[] args = new String[] {key}; - long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LLen), eq(args), any())).thenReturn(testResponse); - - // exercise - CompletableFuture response = service.llen(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lrem_returns_success() { - // setup - String key = "testKey"; - long count = 2L; - String element = "value"; - String[] args = new String[] {key, Long.toString(count), element}; - long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LRem), eq(args), any())).thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lrem(key, count, element); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void rpush_returns_success() { - // setup - String key = "testKey"; - String[] elements = new String[] {"value1", "value2"}; - String[] args = new String[] {key, "value1", "value2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(RPush), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.rpush(key, elements); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void rpop_returns_success() { - // setup - String key = "testKey"; - String value = "value"; - String[] args = new String[] {key}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(RPop), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.rpop(key); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void rpopCount_returns_success() { - // setup - String key = "testKey"; - long count = 2L; - String[] args = new String[] {key, Long.toString(count)}; - String[] value = new String[] {"value1", "value2"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(RPop), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.rpopCount(key, count); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void sadd_returns_success() { - // setup - String key = "testKey"; - String[] members = new String[] {"testMember1", "testMember2"}; - String[] arguments = ArrayUtils.addFirst(members, key); - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SAdd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.sadd(key, members); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void sismember_returns_success() { - // setup - String key = "testKey"; - String member = "testMember"; - String[] arguments = new String[] {key, member}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(true); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SIsMember), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.sismember(key, member); - Boolean payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertTrue(payload); - } - - @SneakyThrows - @Test - public void srem_returns_success() { - // setup - String key = "testKey"; - String[] members = new String[] {"testMember1", "testMember2"}; - String[] arguments = ArrayUtils.addFirst(members, key); - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SRem), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.srem(key, members); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void smembers_returns_success() { - // setup - String key = "testKey"; - Set value = Set.of("testMember"); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(SMembers), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.smembers(key); - Set payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void scard_returns_success() { - // setup - String key = "testKey"; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SCard), eq(new String[] {key}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.scard(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void smismember_returns_success() { - // setup - String key = "testKey"; - String[] members = {"1", "2"}; - String[] arguments = {"testKey", "1", "2"}; - Boolean[] value = {true, false}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SMIsMember), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.smismember(key, members); - Boolean[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void sdiffstore_returns_success() { - // setup - String destination = "dest"; - String[] keys = new String[] {"set1", "set2"}; - String[] arguments = {"dest", "set1", "set2"}; - - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SDiffStore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.sdiffstore(destination, keys); - - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void smove_returns_success() { - // setup - String source = "src"; - String destination = "dst"; - String member = "elem"; - String[] arguments = {source, destination, member}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(true); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SMove), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.smove(source, destination, member); - - // verify - assertEquals(testResponse, response); - assertTrue(response.get()); - } - - @SneakyThrows - @Test - public void sinter_returns_success() { - // setup - String[] keys = new String[] {"key1", "key2"}; - Set value = Set.of("1", "2"); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(SInter), eq(keys), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.sinter(keys); - Set payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void sinterstore_returns_success() { - // setup - String destination = "key"; - String[] keys = new String[] {"set1", "set2"}; - String[] args = new String[] {"key", "set1", "set2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SInterStore), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.sinterstore(destination, keys); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void sunionstore_returns_success() { - // setup - String destination = "key"; - String[] keys = new String[] {"set1", "set2"}; - String[] args = new String[] {"key", "set1", "set2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(SUnionStore), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.sunionstore(destination, keys); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zadd_noOptions_returns_success() { - // setup - String key = "testKey"; - Map membersScores = new LinkedHashMap<>(); - membersScores.put("testMember1", 1.0); - membersScores.put("testMember2", 2.0); - String[] membersScoresArgs = convertMapToValueKeyStringArray(membersScores); - String[] arguments = ArrayUtils.addFirst(membersScoresArgs, key); - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zadd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zadd(key, membersScores, ZaddOptions.builder().build(), false); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zadd_withOptions_returns_success() { - // setup - String key = "testKey"; - ZaddOptions options = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_EXISTS) - .updateOptions(ZaddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) - .build(); - Map membersScores = new LinkedHashMap<>(); - membersScores.put("testMember1", 1.0); - membersScores.put("testMember2", 2.0); - String[] membersScoresArgs = convertMapToValueKeyStringArray(membersScores); - String[] arguments = ArrayUtils.addAll(new String[] {key}, options.toArgs()); - arguments = ArrayUtils.addAll(arguments, membersScoresArgs); - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zadd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zadd(key, membersScores, options, false); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zadd_withIllegalArgument_throws_exception() { - // setup - String key = "testKey"; - ZaddOptions options = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) - .updateOptions(ZaddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) - .build(); - Map membersScores = new LinkedHashMap<>(); - membersScores.put("testMember1", 1.0); - membersScores.put("testMember2", 2.0); - - assertThrows( - IllegalArgumentException.class, () -> service.zadd(key, membersScores, options, false)); - } - - @SneakyThrows - @Test - public void zaddIncr_noOptions_returns_success() { - // setup - String key = "testKey"; - String member = "member"; - double increment = 3.0; - String[] arguments = new String[] {key, "INCR", Double.toString(increment), member}; - Double value = 3.0; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zadd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zaddIncr(key, member, increment, ZaddOptions.builder().build()); - Double payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zaddIncr_withOptions_returns_success() { - // setup - String key = "testKey"; - ZaddOptions options = - ZaddOptions.builder() - .updateOptions(ZaddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) - .build(); - String member = "member"; - double increment = 3.0; - String[] arguments = - concatenateArrays( - new String[] {key}, - options.toArgs(), - new String[] {"INCR", Double.toString(increment), member}); - Double value = 3.0; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zadd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zaddIncr(key, member, increment, options); - Double payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void clientId_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(42L); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.clientId(); - - // verify - assertEquals(testResponse, response); - assertEquals(42L, response.get()); - } - - @SneakyThrows - @Test - public void clientGetName_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete("TEST"); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.clientGetName(); - - // verify - assertEquals(testResponse, response); - assertEquals("TEST", response.get()); - } - - @SneakyThrows - @Test - public void configRewrite_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configRewrite(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void configResetStat_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configResetStat(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void configGet_returns_success() { - // setup - Map testPayload = Map.of("timeout", "1000"); - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(testPayload); - - // match on protobuf request - when(commandManager.>submitNewCommand( - eq(ConfigGet), eq(new String[] {"timeout"}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.configGet(new String[] {"timeout"}); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(testPayload, payload); - } - - @SneakyThrows - @Test - public void configSet_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(ConfigSet), eq(new String[] {"timeout", "1000"}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configSet(Map.of("timeout", "1000")); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, response.get()); - } - - @SneakyThrows - @Test - public void zrem_returns_success() { - // setup - String key = "testKey"; - String[] members = new String[] {"member1", "member2"}; - String[] arguments = ArrayUtils.addFirst(members, key); - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zrem), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrem(key, members); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zcard_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zcard), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zcard(key); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zpopmin_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key}; - Map value = Map.of("member1", 2.5); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(ZPopMin), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.zpopmin(key); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zpopmin_with_count_returns_success() { - // setup - String key = "testKey"; - long count = 2L; - String[] arguments = new String[] {key, Long.toString(count)}; - Map value = Map.of("member1", 2.0, "member2", 3.0); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(ZPopMin), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.zpopmin(key, count); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zpopmax_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key}; - Map value = Map.of("member1", 2.5); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(ZPopMax), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.zpopmax(key); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zpopmax_with_count_returns_success() { - // setup - String key = "testKey"; - long count = 2L; - String[] arguments = new String[] {key, Long.toString(count)}; - Map value = Map.of("member1", 3.0, "member2", 1.0); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(ZPopMax), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.zpopmax(key, count); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zscore_returns_success() { - // setup - String key = "testKey"; - String member = "testMember"; - String[] arguments = new String[] {key, member}; - Double value = 3.5; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZScore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zscore(key, member); - Double payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrange_by_index_returns_success() { - // setup - String key = "testKey"; - RangeByIndex rangeByIndex = new RangeByIndex(0, 1); - String[] arguments = new String[] {key, rangeByIndex.getStart(), rangeByIndex.getEnd()}; - String[] value = new String[] {"one", "two"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zrange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrange(key, rangeByIndex); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrange_by_score_with_reverse_returns_success() { - // setup - String key = "testKey"; - RangeByScore rangeByScore = - new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); - String[] arguments = - new String[] {key, rangeByScore.getStart(), rangeByScore.getEnd(), "BYSCORE", "REV"}; - String[] value = new String[] {"two", "one"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zrange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrange(key, rangeByScore, true); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrange_by_lex_returns_success() { - // setup - String key = "testKey"; - RangeByLex rangeByLex = - new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); - String[] arguments = new String[] {key, rangeByLex.getStart(), rangeByLex.getEnd(), "BYLEX"}; - String[] value = new String[] {"a", "b"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zrange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrange(key, rangeByLex); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrangeWithScores_by_index_returns_success() { - // setup - String key = "testKey"; - RangeByIndex rangeByIndex = new RangeByIndex(0, 4); - String[] arguments = - new String[] {key, rangeByIndex.getStart(), rangeByIndex.getEnd(), WITH_SCORES_REDIS_API}; - Map value = Map.of("one", 1.0, "two", 2.0, "three", 3.0); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(Zrange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.zrangeWithScores(key, rangeByIndex); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrangeWithScores_by_score_returns_success() { - // setup - String key = "testKey"; - RangeByScore rangeByScore = - new RangeByScore( - InfScoreBound.NEGATIVE_INFINITY, - InfScoreBound.POSITIVE_INFINITY, - new RangeOptions.Limit(1, 2)); - String[] arguments = - new String[] { - key, - rangeByScore.getStart(), - rangeByScore.getEnd(), - "BYSCORE", - "LIMIT", - "1", - "2", - WITH_SCORES_REDIS_API - }; - Map value = Map.of("two", 2.0, "three", 3.0); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(Zrange), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = - service.zrangeWithScores(key, rangeByScore, false); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrank_returns_success() { - // setup - String key = "testKey"; - String member = "testMember"; - String[] arguments = new String[] {key, member}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zrank), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrank(key, member); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrankWithScore_returns_success() { - // setup - String key = "testKey"; - String member = "testMember"; - String[] arguments = new String[] {key, member, WITH_SCORE_REDIS_API}; - Object[] value = new Object[] {1, 6.0}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zrank), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrankWithScore(key, member); - Object[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zmscore_returns_success() { - // setup - String key = "testKey"; - String[] members = new String[] {"member1", "member2"}; - String[] arguments = new String[] {key, "member1", "member2"}; - Double[] value = new Double[] {2.5, 8.2}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZMScore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zmscore(key, members); - Double[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zdiffstore_returns_success() { - // setup - String destKey = "testDestKey"; - String[] keys = new String[] {"testKey1", "testKey2"}; - String[] arguments = new String[] {destKey, Long.toString(keys.length), "testKey1", "testKey2"}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZDiffStore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zdiffstore(destKey, keys); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zdiff_returns_success() { - // setup - String key1 = "testKey1"; - String key2 = "testKey2"; - String[] arguments = new String[] {"2", key1, key2}; - String[] value = new String[] {"element1"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZDiff), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zdiff(new String[] {key1, key2}); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zdiffWithScores_returns_success() { - // setup - String key1 = "testKey1"; - String key2 = "testKey2"; - String[] arguments = new String[] {"2", key1, key2, WITH_SCORES_REDIS_API}; - Map value = Map.of("element1", 2.0); - - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.>submitNewCommand(eq(ZDiff), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = - service.zdiffWithScores(new String[] {key1, key2}); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zcount_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key, "-inf", "10.0"}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Zcount), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zcount(key, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(10, true)); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zremrangebyrank_returns_success() { - // setup - String key = "testKey"; - long start = 0; - long end = -1; - String[] arguments = new String[] {key, Long.toString(start), Long.toString(end)}; - Long value = 5L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZRemRangeByRank), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zremrangebyrank(key, start, end); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zremrangebylex_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key, "-", "[z"}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZRemRangeByLex), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zremrangebylex(key, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("z", true)); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zremrangebyscore_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key, "-inf", "10.0"}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZRemRangeByScore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zremrangebyscore(key, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(10, true)); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zlexcount_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key, "-", "[c"}; - Long value = 3L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZLexCount), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zlexcount(key, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", true)); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrangestore_by_lex_returns_success() { - // setup - String source = "testSourceKey"; - String destination = "testDestinationKey"; - RangeByLex rangeByLex = - new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); - String[] arguments = - new String[] {source, destination, rangeByLex.getStart(), rangeByLex.getEnd(), "BYLEX"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrangestore(source, destination, rangeByLex); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrangestore_by_index_returns_success() { - // setup - String source = "testSourceKey"; - String destination = "testDestinationKey"; - RangeByIndex rangeByIndex = new RangeByIndex(0, 1); - String[] arguments = - new String[] {source, destination, rangeByIndex.getStart(), rangeByIndex.getEnd()}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.zrangestore(source, destination, rangeByIndex); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void zrangestore_by_score_with_reverse_returns_success() { - // setup - String source = "testSourceKey"; - String destination = "testDestinationKey"; - RangeByScore rangeByScore = - new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); - boolean reversed = true; - String[] arguments = - new String[] { - source, destination, rangeByScore.getStart(), rangeByScore.getEnd(), "BYSCORE", "REV" - }; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ZRangeStore), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = - service.zrangestore(source, destination, rangeByScore, reversed); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void xadd_returns_success() { - // setup - String key = "testKey"; - Map fieldValues = new LinkedHashMap<>(); - fieldValues.put("testField1", "testValue1"); - fieldValues.put("testField2", "testValue2"); - String[] fieldValuesArgs = convertMapToKeyValueStringArray(fieldValues); - String[] arguments = new String[] {key, "*"}; - arguments = ArrayUtils.addAll(arguments, fieldValuesArgs); - String returnId = "testId"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(returnId); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.xadd(key, fieldValues); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(returnId, payload); - } - - @SneakyThrows - @Test - public void xadd_with_nomakestream_maxlen_options_returns_success() { - // setup - String key = "testKey"; - Map fieldValues = new LinkedHashMap<>(); - fieldValues.put("testField1", "testValue1"); - fieldValues.put("testField2", "testValue2"); - StreamAddOptions options = - StreamAddOptions.builder() - .id("id") - .makeStream(false) - .trim(new StreamAddOptions.MaxLen(true, 5L)) - .build(); - - String[] arguments = - new String[] { - key, - NO_MAKE_STREAM_REDIS_API, - TRIM_MAXLEN_REDIS_API, - TRIM_EXACT_REDIS_API, - Long.toString(5L), - "id" - }; - arguments = ArrayUtils.addAll(arguments, convertMapToKeyValueStringArray(fieldValues)); - - String returnId = "testId"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(returnId); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.xadd(key, fieldValues, options); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(returnId, payload); - } - - private static List getStreamAddOptions() { - return List.of( - Arguments.of( - Pair.of( - // no TRIM option - StreamAddOptions.builder().id("id").makeStream(Boolean.FALSE).build(), - new String[] {"testKey", NO_MAKE_STREAM_REDIS_API, "id"}), - Pair.of( - // MAXLEN with LIMIT - StreamAddOptions.builder() - .id("id") - .makeStream(Boolean.TRUE) - .trim(new StreamAddOptions.MaxLen(Boolean.TRUE, 5L, 10L)) - .build(), - new String[] { - "testKey", - TRIM_MAXLEN_REDIS_API, - TRIM_EXACT_REDIS_API, - Long.toString(5L), - TRIM_LIMIT_REDIS_API, - Long.toString(10L), - "id" - }), - Pair.of( - // MAXLEN with non exact match - StreamAddOptions.builder() - .makeStream(Boolean.FALSE) - .trim(new StreamAddOptions.MaxLen(Boolean.FALSE, 2L)) - .build(), - new String[] { - "testKey", - NO_MAKE_STREAM_REDIS_API, - TRIM_MAXLEN_REDIS_API, - TRIM_NOT_EXACT_REDIS_API, - Long.toString(2L), - "*" - }), - Pair.of( - // MIN ID with LIMIT - StreamAddOptions.builder() - .id("id") - .makeStream(Boolean.TRUE) - .trim(new StreamAddOptions.MinId(Boolean.TRUE, "testKey", 10L)) - .build(), - new String[] { - "testKey", - TRIM_MINID_REDIS_API, - TRIM_EXACT_REDIS_API, - Long.toString(5L), - TRIM_LIMIT_REDIS_API, - Long.toString(10L), - "id" - }), - Pair.of( - // MIN ID with non exact match - StreamAddOptions.builder() - .makeStream(Boolean.FALSE) - .trim(new StreamAddOptions.MinId(Boolean.FALSE, "testKey")) - .build(), - new String[] { - "testKey", - NO_MAKE_STREAM_REDIS_API, - TRIM_MINID_REDIS_API, - TRIM_NOT_EXACT_REDIS_API, - Long.toString(5L), - "*" - }))); - } - - @SneakyThrows - @ParameterizedTest - @MethodSource("getStreamAddOptions") - public void xadd_with_options_returns_success(Pair optionAndArgs) { - // setup - String key = "testKey"; - Map fieldValues = new LinkedHashMap<>(); - fieldValues.put("testField1", "testValue1"); - fieldValues.put("testField2", "testValue2"); - String[] arguments = - ArrayUtils.addAll(optionAndArgs.getRight(), convertMapToKeyValueStringArray(fieldValues)); - - String returnId = "testId"; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(returnId); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(XAdd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.xadd(key, fieldValues, optionAndArgs.getLeft()); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(returnId, payload); - } - - @SneakyThrows - @Test - public void type_returns_success() { - // setup - String key = "testKey"; - String[] arguments = new String[] {key}; - String value = "none"; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Type), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.type(key); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void time_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - String[] payload = new String[] {"UnixTime", "ms"}; - testResponse.complete(payload); - // match on protobuf request - when(commandManager.submitNewCommand(eq(Time), eq(new String[0]), any())) - .thenReturn(testResponse); - // exercise - CompletableFuture response = service.time(); - - // verify - assertEquals(testResponse, response); - assertEquals(payload, response.get()); - } - - @SneakyThrows - @Test - public void lastsave_returns_success() { - // setup - Long value = 42L; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LastSave), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lastsave(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, response.get()); - } - - @SneakyThrows - @Test - public void linsert_returns_success() { - // setup - String key = "testKey"; - var position = BEFORE; - String pivot = "pivot"; - String elem = "elem"; - String[] arguments = new String[] {key, position.toString(), pivot, elem}; - long value = 42; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LInsert), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.linsert(key, position, pivot, elem); - long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void blpop_returns_success() { - // setup - String key = "key"; - double timeout = 0.5; - String[] arguments = new String[] {key, "0.5"}; - String[] value = new String[] {"key", "value"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Blpop), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.blpop(new String[] {key}, timeout); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void rpushx_returns_success() { - // setup - String key = "testKey"; - String[] elements = new String[] {"value1", "value2"}; - String[] args = new String[] {key, "value1", "value2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(RPushX), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.rpushx(key, elements); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void lpushx_returns_success() { - // setup - String key = "testKey"; - String[] elements = new String[] {"value1", "value2"}; - String[] args = new String[] {key, "value1", "value2"}; - Long value = 2L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LPushX), eq(args), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lpushx(key, elements); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void brpop_returns_success() { - // setup - String key = "key"; - double timeout = 0.5; - String[] arguments = new String[] {key, "0.5"}; - String[] value = new String[] {"key", "value"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Brpop), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.brpop(new String[] {key}, timeout); - String[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void pfadd_returns_success() { - // setup - String key = "testKey"; - String[] elements = new String[] {"a", "b", "c"}; - String[] arguments = new String[] {key, "a", "b", "c"}; - Long value = 1L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PfAdd), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.pfadd(key, elements); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - } - - @SneakyThrows - @Test - public void pfcount_returns_success() { - // setup - String[] keys = new String[] {"a", "b", "c"}; - Long value = 1L; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PfCount), eq(keys), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.pfcount(keys); - Long payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, payload); - assertEquals(payload, response.get()); - } - - @SneakyThrows - @Test - public void pfmerge_returns_success() { - // setup - String destKey = "testKey"; - String[] sourceKeys = new String[] {"a", "b", "c"}; - String[] arguments = new String[] {destKey, "a", "b", "c"}; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(PfMerge), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.pfmerge(destKey, sourceKeys); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, response.get()); - } -} diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java deleted file mode 100644 index 89ef402f0a..0000000000 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ /dev/null @@ -1,816 +0,0 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api; - -import static glide.api.BaseClient.OK; -import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; -import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; -import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; -import static redis_request.RedisRequestOuterClass.RequestType.ClientId; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigGet; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; -import static redis_request.RedisRequestOuterClass.RequestType.Echo; -import static redis_request.RedisRequestOuterClass.RequestType.Info; -import static redis_request.RedisRequestOuterClass.RequestType.LastSave; -import static redis_request.RedisRequestOuterClass.RequestType.Ping; -import static redis_request.RedisRequestOuterClass.RequestType.Time; - -import glide.api.models.ClusterTransaction; -import glide.api.models.ClusterValue; -import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.RequestRoutingConfiguration.Route; -import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; -import glide.managers.CommandManager; -import glide.managers.ConnectionManager; -import glide.managers.RedisExceptionCheckedFunction; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import lombok.SneakyThrows; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import redis_request.RedisRequestOuterClass.RedisRequest; -import response.ResponseOuterClass.ConstantResponse; -import response.ResponseOuterClass.Response; - -public class RedisClusterClientTest { - - RedisClusterClient service; - - ConnectionManager connectionManager; - - CommandManager commandManager; - - private final String[] TEST_ARGS = new String[0]; - - @BeforeEach - public void setUp() { - connectionManager = mock(ConnectionManager.class); - commandManager = mock(CommandManager.class); - service = new RedisClusterClient(connectionManager, commandManager); - } - - @Test - @SneakyThrows - public void custom_command_returns_single_value() { - var commandManager = new TestCommandManager(null); - - try (var client = new TestClient(commandManager, "TEST")) { - var value = client.customCommand(TEST_ARGS).get(); - assertEquals("TEST", value.getSingleValue()); - } - } - - @Test - @SneakyThrows - public void custom_command_returns_multi_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("key1", "value1", "key2", "value2"); - try (var client = new TestClient(commandManager, data)) { - var value = client.customCommand(TEST_ARGS).get(); - assertEquals(data, value.getMultiValue()); - } - } - - @Test - @SneakyThrows - // test checks that even a map returned as a single value when single node route is used - public void custom_command_with_single_node_route_returns_single_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("key1", "value1", "key2", "value2"); - try (var client = new TestClient(commandManager, data)) { - var value = client.customCommand(TEST_ARGS, RANDOM).get(); - assertEquals(data, value.getSingleValue()); - } - } - - @Test - @SneakyThrows - public void custom_command_with_multi_node_route_returns_multi_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("key1", "value1", "key2", "value2"); - try (var client = new TestClient(commandManager, data)) { - var value = client.customCommand(TEST_ARGS, ALL_NODES).get(); - assertEquals(data, value.getMultiValue()); - } - } - - @Test - @SneakyThrows - public void custom_command_returns_single_value_on_constant_response() { - var commandManager = - new TestCommandManager( - Response.newBuilder().setConstantResponse(ConstantResponse.OK).build()); - - try (var client = new TestClient(commandManager, "OK")) { - var value = client.customCommand(TEST_ARGS, ALL_NODES).get(); - assertEquals("OK", value.getSingleValue()); - } - } - - private static class TestClient extends RedisClusterClient { - - private final Object object; - - public TestClient(CommandManager commandManager, Object objectToReturn) { - super(null, commandManager); - object = objectToReturn; - } - - @Override - protected T handleRedisResponse(Class classType, boolean isNullable, Response response) { - @SuppressWarnings("unchecked") - T returnValue = (T) object; - return returnValue; - } - - @Override - public void close() {} - } - - private static class TestCommandManager extends CommandManager { - - private final Response response; - - public TestCommandManager(Response responseToReturn) { - super(null); - response = responseToReturn != null ? responseToReturn : Response.newBuilder().build(); - } - - @Override - public CompletableFuture submitCommandToChannel( - RedisRequest.Builder command, RedisExceptionCheckedFunction responseHandler) { - return CompletableFuture.supplyAsync(() -> responseHandler.apply(response)); - } - } - - @SneakyThrows - @Test - public void exec_without_routing() { - // setup - Object[] value = new Object[] {"PONG", "PONG"}; - ClusterTransaction transaction = new ClusterTransaction().ping().ping(); - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(transaction), eq(Optional.empty()), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.exec(transaction); - Object[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertArrayEquals(value, payload); - } - - @SneakyThrows - @Test - public void exec_with_routing() { - // setup - Object[] value = new Object[] {"PONG", "PONG"}; - ClusterTransaction transaction = new ClusterTransaction().ping().ping(); - SingleNodeRoute route = RANDOM; - - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(transaction), eq(Optional.of(route)), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.exec(transaction, route); - Object[] payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertArrayEquals(value, payload); - } - - @SneakyThrows - @Test - public void ping_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete("PONG"); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ping(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals("PONG", payload); - } - - @SneakyThrows - @Test - public void ping_with_message_returns_success() { - // setup - String message = "RETURN OF THE PONG"; - String[] arguments = new String[] {message}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(message); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Ping), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ping(message); - String pong = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(message, pong); - } - - @SneakyThrows - @Test - public void ping_with_route_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete("PONG"); - - Route route = ALL_NODES; - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Ping), eq(new String[0]), eq(route), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ping(route); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals("PONG", payload); - } - - @SneakyThrows - @Test - public void ping_with_message_with_route_returns_success() { - // setup - String message = "RETURN OF THE PONG"; - String[] arguments = new String[] {message}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(message); - - Route route = ALL_PRIMARIES; - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Ping), eq(arguments), eq(route), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.ping(message, route); - String pong = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(message, pong); - } - - @SneakyThrows - @Test - public void echo_returns_success() { - // setup - String message = "GLIDE FOR REDIS"; - String[] arguments = new String[] {message}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(message); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Echo), eq(arguments), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.echo(message); - String echo = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(message, echo); - } - - @SneakyThrows - @Test - public void echo_with_route_returns_success() { - // setup - String message = "GLIDE FOR REDIS"; - String[] arguments = new String[] {message}; - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(ClusterValue.ofSingleValue(message)); - - // match on protobuf request - when(commandManager.>submitNewCommand( - eq(Echo), eq(arguments), eq(RANDOM), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.echo(message, RANDOM); - String echo = response.get().getSingleValue(); - - // verify - assertEquals(testResponse, response); - assertEquals(message, echo); - } - - @SneakyThrows - @Test - public void info_returns_string() { - // setup - Map testPayload = new HashMap<>(); - testPayload.put("addr1", "value1"); - testPayload.put("addr2", "value2"); - testPayload.put("addr3", "value3"); - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(ClusterValue.of(testPayload)); - when(commandManager.>submitNewCommand(eq(Info), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.info(); - - // verify - ClusterValue clusterValue = response.get(); - assertTrue(clusterValue.hasMultiData()); - Map payload = clusterValue.getMultiValue(); - assertEquals(testPayload, payload); - } - - @SneakyThrows - @Test - public void info_with_route_returns_string() { - // setup - Map testClusterValue = Map.of("addr1", "addr1 result", "addr2", "addr2 result"); - Route route = ALL_NODES; - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(ClusterValue.of(testClusterValue)); - when(commandManager.>submitNewCommand( - eq(Info), eq(new String[0]), eq(route), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.info(route); - - // verify - ClusterValue clusterValue = response.get(); - assertTrue(clusterValue.hasMultiData()); - Map clusterMap = clusterValue.getMultiValue(); - assertEquals("addr1 result", clusterMap.get("addr1")); - assertEquals("addr2 result", clusterMap.get("addr2")); - } - - @SneakyThrows - @Test - public void info_with_route_with_infoOptions_returns_string() { - // setup - String[] infoArguments = new String[] {"ALL", "DEFAULT"}; - Map testClusterValue = Map.of("addr1", "addr1 result", "addr2", "addr2 result"); - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(ClusterValue.of(testClusterValue)); - - Route route = ALL_PRIMARIES; - when(commandManager.>submitNewCommand( - eq(Info), eq(infoArguments), eq(route), any())) - .thenReturn(testResponse); - - // exercise - InfoOptions options = - InfoOptions.builder() - .section(InfoOptions.Section.ALL) - .section(InfoOptions.Section.DEFAULT) - .build(); - CompletableFuture> response = service.info(options, route); - - // verify - assertEquals(testResponse.get(), response.get()); - ClusterValue clusterValue = response.get(); - assertTrue(clusterValue.hasMultiData()); - Map clusterMap = clusterValue.getMultiValue(); - assertEquals("addr1 result", clusterMap.get("addr1")); - assertEquals("addr2 result", clusterMap.get("addr2")); - } - - @Test - @SneakyThrows - public void info_with_single_node_route_returns_single_value() { - var commandManager = new TestCommandManager(null); - - var data = "info string"; - try (var client = new TestClient(commandManager, data)) { - var value = client.info(RANDOM).get(); - assertAll( - () -> assertTrue(value.hasSingleData()), - () -> assertEquals(data, value.getSingleValue())); - } - } - - @Test - @SneakyThrows - public void info_with_multi_node_route_returns_multi_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("key1", "value1", "key2", "value2"); - try (var client = new TestClient(commandManager, data)) { - var value = client.info(ALL_NODES).get(); - assertAll( - () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); - } - } - - @Test - @SneakyThrows - public void info_with_options_and_single_node_route_returns_single_value() { - var commandManager = new TestCommandManager(null); - - var data = "info string"; - try (var client = new TestClient(commandManager, data)) { - var value = client.info(InfoOptions.builder().build(), RANDOM).get(); - assertAll( - () -> assertTrue(value.hasSingleData()), - () -> assertEquals(data, value.getSingleValue())); - } - } - - @Test - @SneakyThrows - public void info_with_options_and_multi_node_route_returns_multi_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("key1", "value1", "key2", "value2"); - try (var client = new TestClient(commandManager, data)) { - var value = client.info(InfoOptions.builder().build(), ALL_NODES).get(); - assertAll( - () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); - } - } - - @SneakyThrows - @Test - public void clientId_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(42L); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.clientId(); - - // verify - assertEquals(testResponse, response); - assertEquals(42L, response.get()); - } - - @Test - @SneakyThrows - public void clientId_with_multi_node_route_returns_success() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("n1", 42L); - try (var client = new TestClient(commandManager, data)) { - var value = client.clientId(ALL_NODES).get(); - - assertEquals(data, value.getMultiValue()); - } - } - - @Test - @SneakyThrows - public void clientId_with_single_node_route_returns_success() { - var commandManager = new TestCommandManager(null); - - try (var client = new TestClient(commandManager, 42L)) { - var value = client.clientId(RANDOM).get(); - assertEquals(42, value.getSingleValue()); - } - } - - @SneakyThrows - @Test - public void clientGetName_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete("TEST"); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.clientGetName(); - - // verify - assertEquals(testResponse, response); - assertEquals("TEST", response.get()); - } - - @Test - @SneakyThrows - public void clientGetName_with_single_node_route_returns_success() { - var commandManager = new TestCommandManager(null); - - try (var client = new TestClient(commandManager, "TEST")) { - var value = client.clientGetName(RANDOM).get(); - assertEquals("TEST", value.getSingleValue()); - } - } - - @Test - @SneakyThrows - public void clientGetName_with_multi_node_route_returns_success() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("n1", "TEST"); - try (var client = new TestClient(commandManager, data)) { - var value = client.clientGetName(ALL_NODES).get(); - assertEquals(data, value.getMultiValue()); - } - } - - @SneakyThrows - @Test - public void configRewrite_without_route_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configRewrite(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void configRewrite_with_route_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - Route route = ALL_NODES; - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(ConfigRewrite), eq(new String[0]), eq(route), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configRewrite(route); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void configResetStat_without_route_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configResetStat(); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - @SneakyThrows - @Test - public void configResetStat_with_route_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - Route route = ALL_NODES; - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(ConfigResetStat), eq(new String[0]), eq(route), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configResetStat(route); - String payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, payload); - } - - // TODO copy/move tests from RedisClientTest which call super for coverage - @SneakyThrows - @Test - public void configGet_returns_success() { - // setup - var testPayload = Map.of("timeout", "1000"); - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(testPayload); - - // match on protobuf request - when(commandManager.>submitNewCommand( - eq(ConfigGet), eq(new String[] {"timeout"}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.configGet(new String[] {"timeout"}); - Map payload = response.get(); - - // verify - assertEquals(testResponse, response); - assertEquals(testPayload, payload); - } - - @Test - @SneakyThrows - // test checks that even a map returned as a single value when single node route is used - public void configGet_with_single_node_route_returns_single_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("timeout", "1000", "maxmemory", "1GB"); - try (var client = new TestClient(commandManager, data)) { - var value = client.configGet(TEST_ARGS, RANDOM).get(); - assertAll( - () -> assertTrue(value.hasSingleData()), - () -> assertEquals(data, value.getSingleValue())); - } - } - - @Test - @SneakyThrows - public void configGet_with_multi_node_route_returns_multi_value() { - var commandManager = new TestCommandManager(null); - - var data = Map.of("node1", Map.of("timeout", "1000", "maxmemory", "1GB")); - try (var client = new TestClient(commandManager, data)) { - var value = client.configGet(TEST_ARGS, ALL_NODES).get(); - assertAll( - () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); - } - } - - @SneakyThrows - @Test - public void configSet_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(ConfigSet), eq(new String[] {"timeout", "1000"}), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configSet(Map.of("timeout", "1000")); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, response.get()); - } - - @SneakyThrows - @Test - public void configSet_with_route_returns_success() { - // setup - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(OK); - - // match on protobuf request - when(commandManager.submitNewCommand( - eq(ConfigSet), eq(new String[] {"value", "42"}), eq(RANDOM), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.configSet(Map.of("value", "42"), RANDOM); - - // verify - assertEquals(testResponse, response); - assertEquals(OK, response.get()); - } - - @SneakyThrows - @Test - public void time_returns_success() { - // setup - - String[] payload = new String[] {"UnixTime", "ms"}; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(payload); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(Time), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.time(); - - // verify - assertEquals(testResponse, response); - assertEquals(payload, response.get()); - } - - @SneakyThrows - @Test - public void time_returns_with_route_success() { - // setup - String[] payload = new String[] {"UnixTime", "ms"}; - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(ClusterValue.ofSingleValue(payload)); - - // match on protobuf request - when(commandManager.>submitNewCommand( - eq(Time), eq(new String[0]), eq(RANDOM), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.time(RANDOM); - - // verify - assertEquals(testResponse, response); - assertEquals(payload, response.get().getSingleValue()); - } - - @SneakyThrows - @Test - public void lastsave_returns_success() { - // setup - Long value = 42L; - CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); - - // match on protobuf request - when(commandManager.submitNewCommand(eq(LastSave), eq(new String[0]), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture response = service.lastsave(); - - // verify - assertEquals(testResponse, response); - assertEquals(value, response.get()); - } - - @SneakyThrows - @Test - public void lastsave_returns_with_route_success() { - // setup - Long value = 42L; - CompletableFuture> testResponse = new CompletableFuture<>(); - testResponse.complete(ClusterValue.ofSingleValue(value)); - - // match on protobuf request - when(commandManager.>submitNewCommand( - eq(LastSave), eq(new String[0]), eq(RANDOM), any())) - .thenReturn(testResponse); - - // exercise - CompletableFuture> response = service.lastsave(RANDOM); - - // verify - assertEquals(testResponse, response); - assertEquals(value, response.get().getSingleValue()); - } -} diff --git a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java new file mode 100644 index 0000000000..07e025a2b9 --- /dev/null +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -0,0 +1,92 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models; + +import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import command_request.CommandRequestOuterClass.Command; +import command_request.CommandRequestOuterClass.Command.ArgsArray; +import command_request.CommandRequestOuterClass.RequestType; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Test; + +public class ClusterTransactionTests { + + @Test + public void cluster_transaction_builds_protobuf_request() { + ClusterTransaction transaction = new ClusterTransaction(); + List> results = new LinkedList<>(); + + transaction.publish("msg", "ch1", true); + results.add(Pair.of(SPublish, buildArgs("ch1", "msg"))); + + transaction.sortReadOnly( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sort( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sortStore( + "key1", + "key2", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + ASC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + "key2"))); + + var protobufTransaction = transaction.getProtobufTransaction().build(); + + for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { + Command protobuf = protobufTransaction.getCommands(idx); + + assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); + assertEquals( + results.get(idx).getRight().getArgsCount(), protobuf.getArgsArray().getArgsCount()); + assertEquals(results.get(idx).getRight(), protobuf.getArgsArray()); + } + } +} diff --git a/java/client/src/test/java/glide/api/models/ClusterValueTests.java b/java/client/src/test/java/glide/api/models/ClusterValueTests.java index f74ab21494..d27bb1aaba 100644 --- a/java/client/src/test/java/glide/api/models/ClusterValueTests.java +++ b/java/client/src/test/java/glide/api/models/ClusterValueTests.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.GlideString.gs; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -70,6 +71,24 @@ public void multi_value_ctor() { assertAll( () -> assertTrue(value.hasMultiData()), () -> assertFalse(value.hasSingleData()), - () -> assertNotNull(value.getMultiValue())); + () -> assertNotNull(value.getMultiValue()), + () -> assertTrue(value.getMultiValue().containsKey("config1")), + () -> assertTrue(value.getMultiValue().containsKey("config2"))); + } + + @Test + public void multi_value_binary_ctor() { + var value = + ClusterValue.ofMultiValueBinary( + Map.of(gs("config1"), gs("param1"), gs("config2"), gs("param2"))); + assertAll( + () -> assertTrue(value.hasMultiData()), + () -> assertFalse(value.hasSingleData()), + () -> assertNotNull(value.getMultiValue()), + // ofMultiValueBinary converts the key to a String, but the values are not converted + () -> assertTrue(value.getMultiValue().containsKey("config1")), + () -> assertTrue(value.getMultiValue().get("config1").equals(gs("param1"))), + () -> assertTrue(value.getMultiValue().containsKey("config2")), + () -> assertTrue(value.getMultiValue().get("config2").equals(gs("param2")))); } } diff --git a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java index 2fb78f92a1..5b40988dbf 100644 --- a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java @@ -1,30 +1,204 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.Move; +import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.Select; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; +import static glide.api.commands.GenericCommands.DB_VALKEY_API; +import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.GET_COMMAND_STRING; +import static glide.api.models.commands.scan.BaseScanOptions.COUNT_OPTION_STRING; +import static glide.api.models.commands.scan.BaseScanOptions.MATCH_OPTION_STRING; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.ZSET; +import static glide.api.models.commands.scan.ScanOptions.TYPE_OPTION_STRING; import static org.junit.jupiter.api.Assertions.assertEquals; -import static redis_request.RedisRequestOuterClass.RequestType.Select; +import command_request.CommandRequestOuterClass; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.scan.ScanOptions; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; -import redis_request.RedisRequestOuterClass; -import redis_request.RedisRequestOuterClass.Command.ArgsArray; public class StandaloneTransactionTests { @Test public void standalone_transaction_commands() { - List> + List> results = new LinkedList<>(); Transaction transaction = new Transaction(); transaction.select(5L); - results.add(Pair.of(Select, ArgsArray.newBuilder().addArgs("5").build())); + results.add(Pair.of(Select, buildArgs("5"))); + transaction.move("testKey", 2L); + results.add(Pair.of(Move, buildArgs("testKey", "2"))); + transaction.copy("key1", "key2", 1, true); + results.add(Pair.of(Copy, buildArgs("key1", "key2", DB_VALKEY_API, "1", REPLACE_VALKEY_API))); + + transaction.sort( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sort( + "key1", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + + transaction.scan("cursor"); + results.add(Pair.of(Scan, buildArgs("cursor"))); + + transaction.scan( + "cursor", ScanOptions.builder().matchPattern("pattern").count(99L).type(ZSET).build()); + results.add( + Pair.of( + Scan, + buildArgs( + "cursor", + MATCH_OPTION_STRING, + "pattern", + COUNT_OPTION_STRING, + "99", + TYPE_OPTION_STRING, + ZSET.toString()))); var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { - RedisRequestOuterClass.Command protobuf = protobufTransaction.getCommands(idx); + CommandRequestOuterClass.Command protobuf = protobufTransaction.getCommands(idx); assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); assertEquals( diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index d8ec52fc40..00aed8661f 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -1,111 +1,255 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; -import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; +import static command_request.CommandRequestOuterClass.RequestType.Append; +import static command_request.CommandRequestOuterClass.RequestType.BLMPop; +import static command_request.CommandRequestOuterClass.RequestType.BLMove; +import static command_request.CommandRequestOuterClass.RequestType.BLPop; +import static command_request.CommandRequestOuterClass.RequestType.BRPop; +import static command_request.CommandRequestOuterClass.RequestType.BZMPop; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.BZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.BitCount; +import static command_request.CommandRequestOuterClass.RequestType.BitField; +import static command_request.CommandRequestOuterClass.RequestType.BitFieldReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.BitOp; +import static command_request.CommandRequestOuterClass.RequestType.BitPos; +import static command_request.CommandRequestOuterClass.RequestType.ClientGetName; +import static command_request.CommandRequestOuterClass.RequestType.ClientId; +import static command_request.CommandRequestOuterClass.RequestType.ConfigGet; +import static command_request.CommandRequestOuterClass.RequestType.ConfigResetStat; +import static command_request.CommandRequestOuterClass.RequestType.ConfigRewrite; +import static command_request.CommandRequestOuterClass.RequestType.ConfigSet; +import static command_request.CommandRequestOuterClass.RequestType.Copy; +import static command_request.CommandRequestOuterClass.RequestType.DBSize; +import static command_request.CommandRequestOuterClass.RequestType.Decr; +import static command_request.CommandRequestOuterClass.RequestType.DecrBy; +import static command_request.CommandRequestOuterClass.RequestType.Del; +import static command_request.CommandRequestOuterClass.RequestType.Dump; +import static command_request.CommandRequestOuterClass.RequestType.Echo; +import static command_request.CommandRequestOuterClass.RequestType.Exists; +import static command_request.CommandRequestOuterClass.RequestType.Expire; +import static command_request.CommandRequestOuterClass.RequestType.ExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.ExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.FCall; +import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.FlushAll; +import static command_request.CommandRequestOuterClass.RequestType.FlushDB; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDelete; +import static command_request.CommandRequestOuterClass.RequestType.FunctionDump; +import static command_request.CommandRequestOuterClass.RequestType.FunctionFlush; +import static command_request.CommandRequestOuterClass.RequestType.FunctionList; +import static command_request.CommandRequestOuterClass.RequestType.FunctionLoad; +import static command_request.CommandRequestOuterClass.RequestType.FunctionRestore; +import static command_request.CommandRequestOuterClass.RequestType.FunctionStats; +import static command_request.CommandRequestOuterClass.RequestType.GeoAdd; +import static command_request.CommandRequestOuterClass.RequestType.GeoDist; +import static command_request.CommandRequestOuterClass.RequestType.GeoHash; +import static command_request.CommandRequestOuterClass.RequestType.GeoPos; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearch; +import static command_request.CommandRequestOuterClass.RequestType.GeoSearchStore; +import static command_request.CommandRequestOuterClass.RequestType.Get; +import static command_request.CommandRequestOuterClass.RequestType.GetBit; +import static command_request.CommandRequestOuterClass.RequestType.GetDel; +import static command_request.CommandRequestOuterClass.RequestType.GetEx; +import static command_request.CommandRequestOuterClass.RequestType.GetRange; +import static command_request.CommandRequestOuterClass.RequestType.HDel; +import static command_request.CommandRequestOuterClass.RequestType.HExists; +import static command_request.CommandRequestOuterClass.RequestType.HGet; +import static command_request.CommandRequestOuterClass.RequestType.HGetAll; +import static command_request.CommandRequestOuterClass.RequestType.HIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.HIncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.HKeys; +import static command_request.CommandRequestOuterClass.RequestType.HLen; +import static command_request.CommandRequestOuterClass.RequestType.HMGet; +import static command_request.CommandRequestOuterClass.RequestType.HRandField; +import static command_request.CommandRequestOuterClass.RequestType.HScan; +import static command_request.CommandRequestOuterClass.RequestType.HSet; +import static command_request.CommandRequestOuterClass.RequestType.HSetNX; +import static command_request.CommandRequestOuterClass.RequestType.HStrlen; +import static command_request.CommandRequestOuterClass.RequestType.HVals; +import static command_request.CommandRequestOuterClass.RequestType.Incr; +import static command_request.CommandRequestOuterClass.RequestType.IncrBy; +import static command_request.CommandRequestOuterClass.RequestType.IncrByFloat; +import static command_request.CommandRequestOuterClass.RequestType.Info; +import static command_request.CommandRequestOuterClass.RequestType.LCS; +import static command_request.CommandRequestOuterClass.RequestType.LIndex; +import static command_request.CommandRequestOuterClass.RequestType.LInsert; +import static command_request.CommandRequestOuterClass.RequestType.LLen; +import static command_request.CommandRequestOuterClass.RequestType.LMPop; +import static command_request.CommandRequestOuterClass.RequestType.LMove; +import static command_request.CommandRequestOuterClass.RequestType.LPop; +import static command_request.CommandRequestOuterClass.RequestType.LPos; +import static command_request.CommandRequestOuterClass.RequestType.LPush; +import static command_request.CommandRequestOuterClass.RequestType.LPushX; +import static command_request.CommandRequestOuterClass.RequestType.LRange; +import static command_request.CommandRequestOuterClass.RequestType.LRem; +import static command_request.CommandRequestOuterClass.RequestType.LSet; +import static command_request.CommandRequestOuterClass.RequestType.LTrim; +import static command_request.CommandRequestOuterClass.RequestType.LastSave; +import static command_request.CommandRequestOuterClass.RequestType.Lolwut; +import static command_request.CommandRequestOuterClass.RequestType.MGet; +import static command_request.CommandRequestOuterClass.RequestType.MSet; +import static command_request.CommandRequestOuterClass.RequestType.MSetNX; +import static command_request.CommandRequestOuterClass.RequestType.ObjectEncoding; +import static command_request.CommandRequestOuterClass.RequestType.ObjectFreq; +import static command_request.CommandRequestOuterClass.RequestType.ObjectIdleTime; +import static command_request.CommandRequestOuterClass.RequestType.ObjectRefCount; +import static command_request.CommandRequestOuterClass.RequestType.PExpire; +import static command_request.CommandRequestOuterClass.RequestType.PExpireAt; +import static command_request.CommandRequestOuterClass.RequestType.PExpireTime; +import static command_request.CommandRequestOuterClass.RequestType.PTTL; +import static command_request.CommandRequestOuterClass.RequestType.Persist; +import static command_request.CommandRequestOuterClass.RequestType.PfAdd; +import static command_request.CommandRequestOuterClass.RequestType.PfCount; +import static command_request.CommandRequestOuterClass.RequestType.PfMerge; +import static command_request.CommandRequestOuterClass.RequestType.Ping; +import static command_request.CommandRequestOuterClass.RequestType.Publish; +import static command_request.CommandRequestOuterClass.RequestType.RPop; +import static command_request.CommandRequestOuterClass.RequestType.RPush; +import static command_request.CommandRequestOuterClass.RequestType.RPushX; +import static command_request.CommandRequestOuterClass.RequestType.RandomKey; +import static command_request.CommandRequestOuterClass.RequestType.Rename; +import static command_request.CommandRequestOuterClass.RequestType.RenameNX; +import static command_request.CommandRequestOuterClass.RequestType.Restore; +import static command_request.CommandRequestOuterClass.RequestType.SAdd; +import static command_request.CommandRequestOuterClass.RequestType.SCard; +import static command_request.CommandRequestOuterClass.RequestType.SDiff; +import static command_request.CommandRequestOuterClass.RequestType.SDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.SInter; +import static command_request.CommandRequestOuterClass.RequestType.SInterCard; +import static command_request.CommandRequestOuterClass.RequestType.SInterStore; +import static command_request.CommandRequestOuterClass.RequestType.SIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMIsMember; +import static command_request.CommandRequestOuterClass.RequestType.SMembers; +import static command_request.CommandRequestOuterClass.RequestType.SMove; +import static command_request.CommandRequestOuterClass.RequestType.SPop; +import static command_request.CommandRequestOuterClass.RequestType.SRandMember; +import static command_request.CommandRequestOuterClass.RequestType.SRem; +import static command_request.CommandRequestOuterClass.RequestType.SScan; +import static command_request.CommandRequestOuterClass.RequestType.SUnion; +import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; +import static command_request.CommandRequestOuterClass.RequestType.Set; +import static command_request.CommandRequestOuterClass.RequestType.SetBit; +import static command_request.CommandRequestOuterClass.RequestType.SetRange; +import static command_request.CommandRequestOuterClass.RequestType.Sort; +import static command_request.CommandRequestOuterClass.RequestType.SortReadOnly; +import static command_request.CommandRequestOuterClass.RequestType.Strlen; +import static command_request.CommandRequestOuterClass.RequestType.TTL; +import static command_request.CommandRequestOuterClass.RequestType.Time; +import static command_request.CommandRequestOuterClass.RequestType.Touch; +import static command_request.CommandRequestOuterClass.RequestType.Type; +import static command_request.CommandRequestOuterClass.RequestType.Unlink; +import static command_request.CommandRequestOuterClass.RequestType.Wait; +import static command_request.CommandRequestOuterClass.RequestType.XAck; +import static command_request.CommandRequestOuterClass.RequestType.XAdd; +import static command_request.CommandRequestOuterClass.RequestType.XAutoClaim; +import static command_request.CommandRequestOuterClass.RequestType.XClaim; +import static command_request.CommandRequestOuterClass.RequestType.XDel; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreate; +import static command_request.CommandRequestOuterClass.RequestType.XGroupCreateConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDelConsumer; +import static command_request.CommandRequestOuterClass.RequestType.XGroupDestroy; +import static command_request.CommandRequestOuterClass.RequestType.XGroupSetId; +import static command_request.CommandRequestOuterClass.RequestType.XInfoConsumers; +import static command_request.CommandRequestOuterClass.RequestType.XInfoGroups; +import static command_request.CommandRequestOuterClass.RequestType.XInfoStream; +import static command_request.CommandRequestOuterClass.RequestType.XLen; +import static command_request.CommandRequestOuterClass.RequestType.XPending; +import static command_request.CommandRequestOuterClass.RequestType.XRange; +import static command_request.CommandRequestOuterClass.RequestType.XRead; +import static command_request.CommandRequestOuterClass.RequestType.XReadGroup; +import static command_request.CommandRequestOuterClass.RequestType.XRevRange; +import static command_request.CommandRequestOuterClass.RequestType.XTrim; +import static command_request.CommandRequestOuterClass.RequestType.ZAdd; +import static command_request.CommandRequestOuterClass.RequestType.ZCard; +import static command_request.CommandRequestOuterClass.RequestType.ZCount; +import static command_request.CommandRequestOuterClass.RequestType.ZDiff; +import static command_request.CommandRequestOuterClass.RequestType.ZDiffStore; +import static command_request.CommandRequestOuterClass.RequestType.ZIncrBy; +import static command_request.CommandRequestOuterClass.RequestType.ZInter; +import static command_request.CommandRequestOuterClass.RequestType.ZInterCard; +import static command_request.CommandRequestOuterClass.RequestType.ZInterStore; +import static command_request.CommandRequestOuterClass.RequestType.ZLexCount; +import static command_request.CommandRequestOuterClass.RequestType.ZMPop; +import static command_request.CommandRequestOuterClass.RequestType.ZMScore; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMax; +import static command_request.CommandRequestOuterClass.RequestType.ZPopMin; +import static command_request.CommandRequestOuterClass.RequestType.ZRandMember; +import static command_request.CommandRequestOuterClass.RequestType.ZRange; +import static command_request.CommandRequestOuterClass.RequestType.ZRangeStore; +import static command_request.CommandRequestOuterClass.RequestType.ZRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRem; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByLex; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByRank; +import static command_request.CommandRequestOuterClass.RequestType.ZRemRangeByScore; +import static command_request.CommandRequestOuterClass.RequestType.ZRevRank; +import static command_request.CommandRequestOuterClass.RequestType.ZScan; +import static command_request.CommandRequestOuterClass.RequestType.ZScore; +import static command_request.CommandRequestOuterClass.RequestType.ZUnion; +import static command_request.CommandRequestOuterClass.RequestType.ZUnionStore; +import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; +import static glide.api.commands.HashBaseCommands.WITH_VALUES_VALKEY_API; +import static glide.api.commands.ServerManagementCommands.VERSION_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.COUNT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.LIMIT_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_VALKEY_API; +import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_VALKEY_API; +import static glide.api.commands.StringBaseCommands.IDX_COMMAND_STRING; +import static glide.api.commands.StringBaseCommands.MINMATCHLEN_COMMAND_STRING; +import static glide.api.commands.StringBaseCommands.WITHMATCHLEN_COMMAND_STRING; import static glide.api.models.commands.ExpireOptions.HAS_EXISTING_EXPIRY; import static glide.api.models.commands.ExpireOptions.HAS_NO_EXPIRY; import static glide.api.models.commands.ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT; +import static glide.api.models.commands.FlushMode.ASYNC; import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.LInsertOptions.InsertPosition.AFTER; import static glide.api.models.commands.RangeOptions.InfScoreBound.NEGATIVE_INFINITY; import static glide.api.models.commands.RangeOptions.InfScoreBound.POSITIVE_INFINITY; +import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.ScoreFilter.MIN; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; -import static glide.api.models.commands.ZaddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.WeightAggregateOptions.AGGREGATE_VALKEY_API; +import static glide.api.models.commands.WeightAggregateOptions.WEIGHTS_VALKEY_API; +import static glide.api.models.commands.ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT; +import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_VALKEY_API; +import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_VALKEY_API; +import static glide.api.models.commands.geospatial.GeoAddOptions.CHANGED_VALKEY_API; +import static glide.api.models.commands.geospatial.GeoSearchOrigin.FROMLONLAT_VALKEY_API; +import static glide.api.models.commands.geospatial.GeoSearchOrigin.FROMMEMBER_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.FORCE_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.IDLE_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.JUST_ID_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.RETRY_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.StreamClaimOptions.TIME_VALKEY_API; +import static glide.api.models.commands.stream.StreamGroupOptions.ENTRIES_READ_VALKEY_API; +import static glide.api.models.commands.stream.StreamGroupOptions.MAKE_STREAM_VALKEY_API; +import static glide.api.models.commands.stream.StreamPendingOptions.IDLE_TIME_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.EXCLUSIVE_RANGE_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.MAXIMUM_RANGE_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.MINIMUM_RANGE_VALKEY_API; +import static glide.api.models.commands.stream.StreamRange.RANGE_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadGroupOptions.READ_GROUP_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadGroupOptions.READ_NOACK_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_BLOCK_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_COUNT_VALKEY_API; +import static glide.api.models.commands.stream.StreamReadOptions.READ_STREAMS_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_EXACT_VALKEY_API; +import static glide.api.models.commands.stream.StreamTrimOptions.TRIM_MINID_VALKEY_API; +import static glide.api.models.commands.stream.XInfoStreamOptions.COUNT; +import static glide.api.models.commands.stream.XInfoStreamOptions.FULL; import static org.junit.jupiter.api.Assertions.assertEquals; -import static redis_request.RedisRequestOuterClass.RequestType.Blpop; -import static redis_request.RedisRequestOuterClass.RequestType.Brpop; -import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; -import static redis_request.RedisRequestOuterClass.RequestType.ClientId; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigGet; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; -import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; -import static redis_request.RedisRequestOuterClass.RequestType.Decr; -import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; -import static redis_request.RedisRequestOuterClass.RequestType.Del; -import static redis_request.RedisRequestOuterClass.RequestType.Echo; -import static redis_request.RedisRequestOuterClass.RequestType.Exists; -import static redis_request.RedisRequestOuterClass.RequestType.Expire; -import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.GetRange; -import static redis_request.RedisRequestOuterClass.RequestType.GetString; -import static redis_request.RedisRequestOuterClass.RequestType.HLen; -import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; -import static redis_request.RedisRequestOuterClass.RequestType.HashDel; -import static redis_request.RedisRequestOuterClass.RequestType.HashExists; -import static redis_request.RedisRequestOuterClass.RequestType.HashGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashGetAll; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.HashIncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.HashMGet; -import static redis_request.RedisRequestOuterClass.RequestType.HashSet; -import static redis_request.RedisRequestOuterClass.RequestType.Hvals; -import static redis_request.RedisRequestOuterClass.RequestType.Incr; -import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; -import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; -import static redis_request.RedisRequestOuterClass.RequestType.Info; -import static redis_request.RedisRequestOuterClass.RequestType.LInsert; -import static redis_request.RedisRequestOuterClass.RequestType.LLen; -import static redis_request.RedisRequestOuterClass.RequestType.LPop; -import static redis_request.RedisRequestOuterClass.RequestType.LPush; -import static redis_request.RedisRequestOuterClass.RequestType.LPushX; -import static redis_request.RedisRequestOuterClass.RequestType.LRange; -import static redis_request.RedisRequestOuterClass.RequestType.LRem; -import static redis_request.RedisRequestOuterClass.RequestType.LTrim; -import static redis_request.RedisRequestOuterClass.RequestType.LastSave; -import static redis_request.RedisRequestOuterClass.RequestType.Lindex; -import static redis_request.RedisRequestOuterClass.RequestType.MGet; -import static redis_request.RedisRequestOuterClass.RequestType.MSet; -import static redis_request.RedisRequestOuterClass.RequestType.PExpire; -import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; -import static redis_request.RedisRequestOuterClass.RequestType.PTTL; -import static redis_request.RedisRequestOuterClass.RequestType.Persist; -import static redis_request.RedisRequestOuterClass.RequestType.PfAdd; -import static redis_request.RedisRequestOuterClass.RequestType.PfCount; -import static redis_request.RedisRequestOuterClass.RequestType.PfMerge; -import static redis_request.RedisRequestOuterClass.RequestType.Ping; -import static redis_request.RedisRequestOuterClass.RequestType.RPop; -import static redis_request.RedisRequestOuterClass.RequestType.RPush; -import static redis_request.RedisRequestOuterClass.RequestType.RPushX; -import static redis_request.RedisRequestOuterClass.RequestType.SAdd; -import static redis_request.RedisRequestOuterClass.RequestType.SCard; -import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.SInter; -import static redis_request.RedisRequestOuterClass.RequestType.SInterStore; -import static redis_request.RedisRequestOuterClass.RequestType.SIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; -import static redis_request.RedisRequestOuterClass.RequestType.SMembers; -import static redis_request.RedisRequestOuterClass.RequestType.SMove; -import static redis_request.RedisRequestOuterClass.RequestType.SRem; -import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; -import static redis_request.RedisRequestOuterClass.RequestType.SetRange; -import static redis_request.RedisRequestOuterClass.RequestType.SetString; -import static redis_request.RedisRequestOuterClass.RequestType.Strlen; -import static redis_request.RedisRequestOuterClass.RequestType.TTL; -import static redis_request.RedisRequestOuterClass.RequestType.Time; -import static redis_request.RedisRequestOuterClass.RequestType.Type; -import static redis_request.RedisRequestOuterClass.RequestType.Unlink; -import static redis_request.RedisRequestOuterClass.RequestType.XAdd; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; -import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZLexCount; -import static redis_request.RedisRequestOuterClass.RequestType.ZMScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMax; -import static redis_request.RedisRequestOuterClass.RequestType.ZPopMin; -import static redis_request.RedisRequestOuterClass.RequestType.ZRangeStore; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByLex; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByRank; -import static redis_request.RedisRequestOuterClass.RequestType.ZRemRangeByScore; -import static redis_request.RedisRequestOuterClass.RequestType.ZScore; -import static redis_request.RedisRequestOuterClass.RequestType.Zadd; -import static redis_request.RedisRequestOuterClass.RequestType.Zcard; -import static redis_request.RedisRequestOuterClass.RequestType.Zcount; -import static redis_request.RedisRequestOuterClass.RequestType.Zrange; -import static redis_request.RedisRequestOuterClass.RequestType.Zrank; -import static redis_request.RedisRequestOuterClass.RequestType.Zrem; +import com.google.protobuf.ByteString; +import command_request.CommandRequestOuterClass.Command; +import command_request.CommandRequestOuterClass.Command.ArgsArray; +import command_request.CommandRequestOuterClass.RequestType; +import glide.api.models.commands.ConditionalChange; +import glide.api.models.commands.GetExOptions; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; @@ -115,8 +259,43 @@ import glide.api.models.commands.RangeOptions.RangeByScore; import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.SetOptions; -import glide.api.models.commands.StreamAddOptions; -import glide.api.models.commands.ZaddOptions; +import glide.api.models.commands.SortOrder; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; +import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.geospatial.GeoAddOptions; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamPendingOptions; +import glide.api.models.commands.stream.StreamRange; +import glide.api.models.commands.stream.StreamRange.InfRangeBound; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions.MinId; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -126,9 +305,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import redis_request.RedisRequestOuterClass.Command; -import redis_request.RedisRequestOuterClass.Command.ArgsArray; -import redis_request.RedisRequestOuterClass.RequestType; public class TransactionTests { private static Stream getTransactionBuilders() { @@ -141,13 +317,22 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) List> results = new LinkedList<>(); transaction.get("key"); - results.add(Pair.of(GetString, buildArgs("key"))); + results.add(Pair.of(Get, buildArgs("key"))); + + transaction.getex("key"); + results.add(Pair.of(GetEx, buildArgs("key"))); + + transaction.getex("key", GetExOptions.Seconds(10L)); + results.add(Pair.of(GetEx, buildArgs("key", "EX", "10"))); transaction.set("key", "value"); - results.add(Pair.of(SetString, buildArgs("key", "value"))); + results.add(Pair.of(Set, buildArgs("key", "value"))); transaction.set("key", "value", SetOptions.builder().returnOldValue(true).build()); - results.add(Pair.of(SetString, buildArgs("key", "value", RETURN_OLD_VALUE))); + results.add(Pair.of(Set, buildArgs("key", "value", RETURN_OLD_VALUE))); + + transaction.append("key", "value"); + results.add(Pair.of(Append, buildArgs("key", "value"))); transaction.del(new String[] {"key1", "key2"}); results.add(Pair.of(Del, buildArgs("key1", "key2"))); @@ -170,6 +355,9 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.mset(Map.of("key", "value")); results.add(Pair.of(MSet, buildArgs("key", "value"))); + transaction.msetnx(Map.of("key", "value")); + results.add(Pair.of(MSetNX, buildArgs("key", "value"))); + transaction.mget(new String[] {"key"}); results.add(Pair.of(MGet, buildArgs("key"))); @@ -198,41 +386,67 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) results.add(Pair.of(GetRange, buildArgs("key", "42", "54"))); transaction.hset("key", Map.of("field", "value")); - results.add(Pair.of(HashSet, buildArgs("key", "field", "value"))); + results.add(Pair.of(HSet, buildArgs("key", "field", "value"))); transaction.hsetnx("key", "field", "value"); results.add(Pair.of(HSetNX, buildArgs("key", "field", "value"))); transaction.hget("key", "field"); - results.add(Pair.of(HashGet, buildArgs("key", "field"))); + results.add(Pair.of(HGet, buildArgs("key", "field"))); transaction.hdel("key", new String[] {"field"}); - results.add(Pair.of(HashDel, buildArgs("key", "field"))); + results.add(Pair.of(HDel, buildArgs("key", "field"))); transaction.hlen("key"); results.add(Pair.of(HLen, buildArgs("key"))); transaction.hvals("key"); - results.add(Pair.of(Hvals, buildArgs("key"))); + results.add(Pair.of(HVals, buildArgs("key"))); transaction.hmget("key", new String[] {"field"}); - results.add(Pair.of(HashMGet, buildArgs("key", "field"))); + results.add(Pair.of(HMGet, buildArgs("key", "field"))); transaction.hexists("key", "field"); - results.add(Pair.of(HashExists, buildArgs("key", "field"))); + results.add(Pair.of(HExists, buildArgs("key", "field"))); transaction.hgetall("key"); - results.add(Pair.of(HashGetAll, buildArgs("key"))); + results.add(Pair.of(HGetAll, buildArgs("key"))); transaction.hincrBy("key", "field", 1); - results.add(Pair.of(HashIncrBy, buildArgs("key", "field", "1"))); + results.add(Pair.of(HIncrBy, buildArgs("key", "field", "1"))); transaction.hincrByFloat("key", "field", 1.5); - results.add(Pair.of(HashIncrByFloat, buildArgs("key", "field", "1.5"))); + results.add(Pair.of(HIncrByFloat, buildArgs("key", "field", "1.5"))); + + transaction.hkeys("key"); + results.add(Pair.of(HKeys, buildArgs("key"))); + + transaction.hstrlen("key", "field"); + results.add(Pair.of(HStrlen, buildArgs("key", "field"))); + + transaction + .hrandfield("key") + .hrandfieldWithCount("key", 2) + .hrandfieldWithCountWithValues("key", 3); + results.add(Pair.of(HRandField, buildArgs("key"))); + results.add(Pair.of(HRandField, buildArgs("key", "2"))); + results.add(Pair.of(HRandField, buildArgs("key", "3", WITH_VALUES_VALKEY_API))); transaction.lpush("key", new String[] {"element1", "element2"}); results.add(Pair.of(LPush, buildArgs("key", "element1", "element2"))); + transaction.lpos("key", "element1"); + results.add(Pair.of(LPos, buildArgs("key", "element1"))); + + transaction.lpos("key", "element1", LPosOptions.builder().rank(1L).build()); + results.add(Pair.of(LPos, buildArgs("key", "element1", "RANK", "1"))); + + transaction.lposCount("key", "element1", 1L); + results.add(Pair.of(LPos, buildArgs("key", "element1", "COUNT", "1"))); + + transaction.lposCount("key", "element1", 1L, LPosOptions.builder().rank(1L).build()); + results.add(Pair.of(LPos, buildArgs("key", "element1", "COUNT", "1", "RANK", "1"))); + transaction.lpop("key"); results.add(Pair.of(LPop, buildArgs("key"))); @@ -243,7 +457,7 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) results.add(Pair.of(LRange, buildArgs("key", "1", "2"))); transaction.lindex("key", 1); - results.add(Pair.of(Lindex, ArgsArray.newBuilder().addArgs("key").addArgs("1").build())); + results.add(Pair.of(LIndex, buildArgs("key", "1"))); transaction.ltrim("key", 1, 2); results.add(Pair.of(LTrim, buildArgs("key", "1", "2"))); @@ -267,8 +481,7 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) results.add(Pair.of(SAdd, buildArgs("key", "value"))); transaction.sismember("key", "member"); - results.add( - Pair.of(SIsMember, ArgsArray.newBuilder().addArgs("key").addArgs("member").build())); + results.add(Pair.of(SIsMember, buildArgs("key", "member"))); transaction.srem("key", new String[] {"value"}); results.add(Pair.of(SRem, buildArgs("key", "value"))); @@ -292,10 +505,7 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) results.add(Pair.of(SMIsMember, buildArgs("key", "1", "2"))); transaction.sunionstore("key", new String[] {"set1", "set2"}); - results.add( - Pair.of( - SUnionStore, - ArgsArray.newBuilder().addArgs("key").addArgs("set1").addArgs("set2").build())); + results.add(Pair.of(SUnionStore, buildArgs("key", "set1", "set2"))); transaction.exists(new String[] {"key1", "key2"}); results.add(Pair.of(Exists, buildArgs("key1", "key2"))); @@ -320,12 +530,21 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.pexpireAt("key", 99999999L, HAS_NO_EXPIRY); results.add(Pair.of(PExpireAt, buildArgs("key", "99999999", "NX"))); + transaction.getdel("key"); + results.add(Pair.of(GetDel, buildArgs("key"))); + transaction.ttl("key"); results.add(Pair.of(TTL, buildArgs("key"))); transaction.pttl("key"); results.add(Pair.of(PTTL, buildArgs("key"))); + transaction.expiretime("key"); + results.add(Pair.of(ExpireTime, buildArgs("key"))); + + transaction.pexpiretime("key"); + results.add(Pair.of(PExpireTime, buildArgs("key"))); + transaction.clientId(); results.add(Pair.of(ClientId, buildArgs())); @@ -354,22 +573,22 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.zadd( "key", membersScores, - ZaddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build(), + ZAddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build(), true); - results.add(Pair.of(Zadd, buildArgs("key", "LT", "CH", "1.0", "member1", "2.0", "member2"))); + results.add(Pair.of(ZAdd, buildArgs("key", "LT", "CH", "1.0", "member1", "2.0", "member2"))); transaction.zaddIncr( "key", "member1", 3.0, - ZaddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build()); - results.add(Pair.of(Zadd, buildArgs("key", "LT", "INCR", "3.0", "member1"))); + ZAddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build()); + results.add(Pair.of(ZAdd, buildArgs("key", "LT", "INCR", "3.0", "member1"))); transaction.zrem("key", new String[] {"member1", "member2"}); - results.add(Pair.of(Zrem, buildArgs("key", "member1", "member2"))); + results.add(Pair.of(ZRem, buildArgs("key", "member1", "member2"))); transaction.zcard("key"); - results.add(Pair.of(Zcard, buildArgs("key"))); + results.add(Pair.of(ZCard, buildArgs("key"))); transaction.zpopmin("key"); results.add(Pair.of(ZPopMin, buildArgs("key"))); @@ -377,9 +596,15 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.zpopmin("key", 2); results.add(Pair.of(ZPopMin, buildArgs("key", "2"))); + transaction.bzpopmin(new String[] {"key1", "key2"}, .5); + results.add(Pair.of(BZPopMin, buildArgs("key1", "key2", "0.5"))); + transaction.zpopmax("key"); results.add(Pair.of(ZPopMax, buildArgs("key"))); + transaction.bzpopmax(new String[] {"key1", "key2"}, .5); + results.add(Pair.of(BZPopMax, buildArgs("key1", "key2", "0.5"))); + transaction.zpopmax("key", 2); results.add(Pair.of(ZPopMax, buildArgs("key", "2"))); @@ -387,10 +612,16 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) results.add(Pair.of(ZScore, buildArgs("key", "member"))); transaction.zrank("key", "member"); - results.add(Pair.of(Zrank, buildArgs("key", "member"))); + results.add(Pair.of(ZRank, buildArgs("key", "member"))); transaction.zrankWithScore("key", "member"); - results.add(Pair.of(Zrank, buildArgs("key", "member", WITH_SCORE_REDIS_API))); + results.add(Pair.of(ZRank, buildArgs("key", "member", WITH_SCORE_VALKEY_API))); + + transaction.zrevrank("key", "member"); + results.add(Pair.of(ZRevRank, buildArgs("key", "member"))); + + transaction.zrevrankWithScore("key", "member"); + results.add(Pair.of(ZRevRank, buildArgs("key", "member", WITH_SCORE_VALKEY_API))); transaction.zmscore("key", new String[] {"member1", "member2"}); results.add(Pair.of(ZMScore, buildArgs("key", "member1", "member2"))); @@ -399,21 +630,23 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) results.add(Pair.of(ZDiff, buildArgs("2", "key1", "key2"))); transaction.zdiffWithScores(new String[] {"key1", "key2"}); - results.add( - Pair.of( - ZDiff, - ArgsArray.newBuilder() - .addArgs("2") - .addArgs("key1") - .addArgs("key2") - .addArgs(WITH_SCORES_REDIS_API) - .build())); + results.add(Pair.of(ZDiff, buildArgs("2", "key1", "key2", WITH_SCORES_VALKEY_API))); transaction.zdiffstore("destKey", new String[] {"key1", "key2"}); results.add(Pair.of(ZDiffStore, buildArgs("destKey", "2", "key1", "key2"))); + transaction.zmpop(new String[] {"key1", "key2"}, MAX).zmpop(new String[] {"key"}, MIN, 42); + results.add(Pair.of(ZMPop, buildArgs("2", "key1", "key2", "MAX"))); + results.add(Pair.of(ZMPop, buildArgs("1", "key", "MIN", "COUNT", "42"))); + + transaction + .bzmpop(new String[] {"key1", "key2"}, MAX, .1) + .bzmpop(new String[] {"key"}, MIN, .1, 42); + results.add(Pair.of(BZMPop, buildArgs("0.1", "2", "key1", "key2", "MAX"))); + results.add(Pair.of(BZMPop, buildArgs("0.1", "1", "key", "MIN", "COUNT", "42"))); + transaction.zcount("key", new ScoreBoundary(5, false), InfScoreBound.POSITIVE_INFINITY); - results.add(Pair.of(Zcount, buildArgs("key", "(5.0", "+inf"))); + results.add(Pair.of(ZCount, buildArgs("key", "(5.0", "+inf"))); transaction.zremrangebyrank("key", 0, -1); results.add(Pair.of(ZRemRangeByRank, buildArgs("key", "0", "-1"))); @@ -443,31 +676,377 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.zrangestore("destination", "source", new RangeByIndex(2, 3)); results.add(Pair.of(ZRangeStore, buildArgs("destination", "source", "2", "3"))); + transaction.zinterstore("destination", new KeyArray(new String[] {"key1", "key2"})); + results.add(Pair.of(ZInterStore, buildArgs("destination", "2", "key1", "key2"))); + + transaction.zunionstore("destination", new KeyArray(new String[] {"key1", "key2"})); + results.add(Pair.of(ZUnionStore, buildArgs("destination", "2", "key1", "key2"))); + + transaction.zunion(new KeyArray(new String[] {"key1", "key2"})); + results.add(Pair.of(ZUnion, buildArgs("2", "key1", "key2"))); + + transaction.zunionWithScores(new KeyArray(new String[] {"key1", "key2"})); + results.add(Pair.of(ZUnion, buildArgs("2", "key1", "key2", WITH_SCORES_VALKEY_API))); + + List> weightedKeys = new ArrayList<>(); + weightedKeys.add(Pair.of("key1", 10.0)); + weightedKeys.add(Pair.of("key2", 20.0)); + + transaction.zinterstore("destination", new WeightedKeys(weightedKeys), Aggregate.MAX); + results.add( + Pair.of( + ZInterStore, + buildArgs( + "destination", + "2", + "key1", + "key2", + WEIGHTS_VALKEY_API, + "10.0", + "20.0", + AGGREGATE_VALKEY_API, + Aggregate.MAX.toString()))); + + transaction.zunionstore("destination", new WeightedKeys(weightedKeys), Aggregate.MAX); + results.add( + Pair.of( + ZUnionStore, + buildArgs( + "destination", + "2", + "key1", + "key2", + WEIGHTS_VALKEY_API, + "10.0", + "20.0", + AGGREGATE_VALKEY_API, + Aggregate.MAX.toString()))); + + transaction.zunionWithScores(new WeightedKeys(weightedKeys), Aggregate.MAX); + results.add( + Pair.of( + ZUnion, + buildArgs( + "2", + "key1", + "key2", + WEIGHTS_VALKEY_API, + "10.0", + "20.0", + AGGREGATE_VALKEY_API, + Aggregate.MAX.toString(), + WITH_SCORES_VALKEY_API))); + transaction.zintercard(new String[] {"key1", "key2"}, 5); + results.add(Pair.of(ZInterCard, buildArgs("2", "key1", "key2", LIMIT_VALKEY_API, "5"))); + + transaction.zintercard(new String[] {"key1", "key2"}); + results.add(Pair.of(ZInterCard, buildArgs("2", "key1", "key2"))); + + transaction.zinter(new KeyArray(new String[] {"key1", "key2"})); + results.add(Pair.of(ZInter, buildArgs("2", "key1", "key2"))); + + transaction.zinterWithScores(new KeyArray(new String[] {"key1", "key2"})); + results.add(Pair.of(ZInter, buildArgs("2", "key1", "key2", WITH_SCORES_VALKEY_API))); + + transaction.zinterWithScores(new WeightedKeys(weightedKeys), Aggregate.MAX); + results.add( + Pair.of( + ZInter, + buildArgs( + "2", + "key1", + "key2", + WEIGHTS_VALKEY_API, + "10.0", + "20.0", + AGGREGATE_VALKEY_API, + Aggregate.MAX.toString(), + WITH_SCORES_VALKEY_API))); + transaction.xadd("key", Map.of("field1", "foo1")); results.add(Pair.of(XAdd, buildArgs("key", "*", "field1", "foo1"))); transaction.xadd("key", Map.of("field1", "foo1"), StreamAddOptions.builder().id("id").build()); results.add(Pair.of(XAdd, buildArgs("key", "id", "field1", "foo1"))); + transaction.xtrim("key", new MinId(true, "id")); + results.add( + Pair.of(XTrim, buildArgs("key", TRIM_MINID_VALKEY_API, TRIM_EXACT_VALKEY_API, "id"))); + + transaction.xread(Map.of("key", "id")); + results.add(Pair.of(XRead, buildArgs(READ_STREAMS_VALKEY_API, "key", "id"))); + + transaction.xread(Map.of("key", "id"), StreamReadOptions.builder().block(1L).count(2L).build()); + results.add( + Pair.of( + XRead, + buildArgs( + READ_COUNT_VALKEY_API, + "2", + READ_BLOCK_VALKEY_API, + "1", + READ_STREAMS_VALKEY_API, + "key", + "id"))); + + transaction.xlen("key"); + results.add(Pair.of(XLen, buildArgs("key"))); + + transaction.xdel("key", new String[] {"12345-1", "98765-4"}); + results.add(Pair.of(XDel, buildArgs("key", "12345-1", "98765-4"))); + + transaction.xrange("key", InfRangeBound.MIN, InfRangeBound.MAX); + results.add( + Pair.of(XRange, buildArgs("key", MINIMUM_RANGE_VALKEY_API, MAXIMUM_RANGE_VALKEY_API))); + + transaction.xrange("key", InfRangeBound.MIN, InfRangeBound.MAX, 99L); + results.add( + Pair.of( + XRange, + buildArgs( + "key", + MINIMUM_RANGE_VALKEY_API, + MAXIMUM_RANGE_VALKEY_API, + RANGE_COUNT_VALKEY_API, + "99"))); + + transaction.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN); + results.add( + Pair.of(XRevRange, buildArgs("key", MAXIMUM_RANGE_VALKEY_API, MINIMUM_RANGE_VALKEY_API))); + + transaction.xrevrange("key", InfRangeBound.MAX, InfRangeBound.MIN, 99L); + results.add( + Pair.of( + XRevRange, + buildArgs( + "key", + MAXIMUM_RANGE_VALKEY_API, + MINIMUM_RANGE_VALKEY_API, + RANGE_COUNT_VALKEY_API, + "99"))); + + transaction.xgroupCreate("key", "group", "id"); + results.add(Pair.of(XGroupCreate, buildArgs("key", "group", "id"))); + + transaction.xgroupCreate( + "key", "group", "id", StreamGroupOptions.builder().makeStream().entriesRead(123L).build()); + results.add( + Pair.of( + XGroupCreate, + buildArgs( + "key", "group", "id", MAKE_STREAM_VALKEY_API, ENTRIES_READ_VALKEY_API, "123"))); + + transaction.xgroupDestroy("key", "group"); + results.add(Pair.of(XGroupDestroy, buildArgs("key", "group"))); + + transaction.xgroupCreateConsumer("key", "group", "consumer"); + results.add(Pair.of(XGroupCreateConsumer, buildArgs("key", "group", "consumer"))); + + transaction.xgroupDelConsumer("key", "group", "consumer"); + results.add(Pair.of(XGroupDelConsumer, buildArgs("key", "group", "consumer"))); + + transaction.xreadgroup(Map.of("key", "id"), "group", "consumer"); + results.add( + Pair.of( + XReadGroup, + buildArgs( + READ_GROUP_VALKEY_API, "group", "consumer", READ_STREAMS_VALKEY_API, "key", "id"))); + + transaction.xgroupSetId("key", "group", "id"); + results.add(Pair.of(XGroupSetId, buildArgs("key", "group", "id"))); + + transaction.xgroupSetId("key", "group", "id", 1); + results.add(Pair.of(XGroupSetId, buildArgs("key", "group", "id", "ENTRIESREAD", "1"))); + + transaction.xreadgroup( + Map.of("key", "id"), + "group", + "consumer", + StreamReadGroupOptions.builder().block(1L).count(2L).noack().build()); + results.add( + Pair.of( + XReadGroup, + buildArgs( + READ_GROUP_VALKEY_API, + "group", + "consumer", + READ_COUNT_VALKEY_API, + "2", + READ_BLOCK_VALKEY_API, + "1", + READ_NOACK_VALKEY_API, + READ_STREAMS_VALKEY_API, + "key", + "id"))); + + transaction.xack("key", "group", new String[] {"12345-1", "98765-4"}); + results.add(Pair.of(XAck, buildArgs("key", "group", "12345-1", "98765-4"))); + + transaction.xpending("key", "group"); + results.add(Pair.of(XPending, buildArgs("key", "group"))); + + transaction.xpending("key", "group", InfRangeBound.MAX, InfRangeBound.MIN, 99L); + results.add( + Pair.of( + XPending, + buildArgs("key", "group", MAXIMUM_RANGE_VALKEY_API, MINIMUM_RANGE_VALKEY_API, "99"))); + + transaction.xpending( + "key", + "group", + StreamRange.IdBound.ofExclusive("11"), + StreamRange.IdBound.ofExclusive("1234-0"), + 99L, + StreamPendingOptions.builder().minIdleTime(5L).consumer("consumer").build()); + results.add( + Pair.of( + XPending, + buildArgs( + "key", + "group", + IDLE_TIME_VALKEY_API, + "5", + EXCLUSIVE_RANGE_VALKEY_API + "11", + EXCLUSIVE_RANGE_VALKEY_API + "1234-0", + "99", + "consumer"))); + + transaction.xinfoStream("key").xinfoStreamFull("key").xinfoStreamFull("key", 42); + results.add(Pair.of(XInfoStream, buildArgs("key"))); + results.add(Pair.of(XInfoStream, buildArgs("key", FULL))); + results.add(Pair.of(XInfoStream, buildArgs("key", FULL, COUNT, "42"))); + + transaction.xclaim("key", "group", "consumer", 99L, new String[] {"12345-1", "98765-4"}); + results.add(Pair.of(XClaim, buildArgs("key", "group", "consumer", "99", "12345-1", "98765-4"))); + + StreamClaimOptions claimOptions = + StreamClaimOptions.builder().force().idle(11L).idleUnixTime(12L).retryCount(5L).build(); + transaction.xclaim( + "key", "group", "consumer", 99L, new String[] {"12345-1", "98765-4"}, claimOptions); + results.add( + Pair.of( + XClaim, + buildArgs( + "key", + "group", + "consumer", + "99", + "12345-1", + "98765-4", + IDLE_VALKEY_API, + "11", + TIME_VALKEY_API, + "12", + RETRY_COUNT_VALKEY_API, + "5", + FORCE_VALKEY_API))); + + transaction.xclaimJustId("key", "group", "consumer", 99L, new String[] {"12345-1", "98765-4"}); + results.add( + Pair.of( + XClaim, + buildArgs("key", "group", "consumer", "99", "12345-1", "98765-4", JUST_ID_VALKEY_API))); + + transaction.xclaimJustId( + "key", "group", "consumer", 99L, new String[] {"12345-1", "98765-4"}, claimOptions); + results.add( + Pair.of( + XClaim, + buildArgs( + "key", + "group", + "consumer", + "99", + "12345-1", + "98765-4", + IDLE_VALKEY_API, + "11", + TIME_VALKEY_API, + "12", + RETRY_COUNT_VALKEY_API, + "5", + FORCE_VALKEY_API, + JUST_ID_VALKEY_API))); + + transaction.xinfoGroups("key"); + results.add(Pair.of(XInfoGroups, buildArgs("key"))); + + transaction.xinfoConsumers("key", "groupName"); + results.add(Pair.of(XInfoConsumers, buildArgs("key", "groupName"))); + + transaction.xautoclaim("key", "group", "consumer", 99L, "0-0"); + results.add(Pair.of(XAutoClaim, buildArgs("key", "group", "consumer", "99", "0-0"))); + + transaction.xautoclaim("key", "group", "consumer", 99L, "0-0", 1234L); + results.add( + Pair.of(XAutoClaim, buildArgs("key", "group", "consumer", "99", "0-0", "COUNT", "1234"))); + + transaction.xautoclaimJustId("key", "group", "consumer", 99L, "0-0"); + results.add(Pair.of(XAutoClaim, buildArgs("key", "group", "consumer", "99", "0-0", "JUSTID"))); + + transaction.xautoclaimJustId("key", "group", "consumer", 99L, "0-0", 1234L); + results.add( + Pair.of( + XAutoClaim, + buildArgs("key", "group", "consumer", "99", "0-0", "COUNT", "1234", "JUSTID"))); + transaction.time(); results.add(Pair.of(Time, buildArgs())); transaction.lastsave(); results.add(Pair.of(LastSave, buildArgs())); + transaction.flushall().flushall(ASYNC); + results.add(Pair.of(FlushAll, buildArgs())); + results.add(Pair.of(FlushAll, buildArgs(ASYNC.toString()))); + + transaction.flushdb().flushdb(ASYNC); + results.add(Pair.of(FlushDB, buildArgs())); + results.add(Pair.of(FlushDB, buildArgs(ASYNC.toString()))); + + transaction.lolwut().lolwut(5).lolwut(new int[] {1, 2}).lolwut(6, new int[] {42}); + results.add(Pair.of(Lolwut, buildArgs())); + results.add(Pair.of(Lolwut, buildArgs(VERSION_VALKEY_API, "5"))); + results.add(Pair.of(Lolwut, buildArgs("1", "2"))); + results.add(Pair.of(Lolwut, buildArgs(VERSION_VALKEY_API, "6", "42"))); + + transaction.dbsize(); + results.add(Pair.of(DBSize, buildArgs())); + transaction.persist("key"); results.add(Pair.of(Persist, buildArgs("key"))); + transaction.zrandmember("key"); + results.add(Pair.of(ZRandMember, buildArgs("key"))); + + transaction.zrandmemberWithCount("key", 5); + results.add(Pair.of(ZRandMember, buildArgs("key", "5"))); + + transaction.zrandmemberWithCountWithScores("key", 5); + results.add(Pair.of(ZRandMember, buildArgs("key", "5", WITH_SCORES_VALKEY_API))); + + transaction.zincrby("key", 3.14, "value"); + results.add(Pair.of(ZIncrBy, buildArgs("key", "3.14", "value"))); + transaction.type("key"); results.add(Pair.of(Type, buildArgs("key"))); + transaction.randomKey(); + results.add(Pair.of(RandomKey, buildArgs())); + + transaction.rename("key", "newKey"); + results.add(Pair.of(Rename, buildArgs("key", "newKey"))); + + transaction.renamenx("key", "newKey"); + results.add(Pair.of(RenameNX, buildArgs("key", "newKey"))); + transaction.linsert("key", AFTER, "pivot", "elem"); results.add(Pair.of(LInsert, buildArgs("key", "AFTER", "pivot", "elem"))); transaction.brpop(new String[] {"key1", "key2"}, 0.5); - results.add(Pair.of(Brpop, buildArgs("key1", "key2", "0.5"))); + results.add(Pair.of(BRPop, buildArgs("key1", "key2", "0.5"))); transaction.blpop(new String[] {"key1", "key2"}, 0.5); - results.add(Pair.of(Blpop, buildArgs("key1", "key2", "0.5"))); + results.add(Pair.of(BLPop, buildArgs("key1", "key2", "0.5"))); transaction.rpushx("key", new String[] {"element1", "element2"}); results.add(Pair.of(RPushX, buildArgs("key", "element1", "element2"))); @@ -480,7 +1059,7 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), true); results.add( - Pair.of(Zrange, buildArgs("key", "-inf", "(3.0", "BYSCORE", "REV", "LIMIT", "1", "2"))); + Pair.of(ZRange, buildArgs("key", "-inf", "(3.0", "BYSCORE", "REV", "LIMIT", "1", "2"))); transaction.zrangeWithScores( "key", @@ -488,25 +1067,446 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), false); results.add( Pair.of( - Zrange, - buildArgs("key", "5.0", "+inf", "BYSCORE", "LIMIT", "1", "2", WITH_SCORES_REDIS_API))); + ZRange, + buildArgs("key", "5.0", "+inf", "BYSCORE", "LIMIT", "1", "2", WITH_SCORES_VALKEY_API))); transaction.pfadd("hll", new String[] {"a", "b", "c"}); results.add(Pair.of(PfAdd, buildArgs("hll", "a", "b", "c"))); transaction.pfcount(new String[] {"hll1", "hll2"}); - results.add(Pair.of(PfCount, ArgsArray.newBuilder().addArgs("hll1").addArgs("hll2").build())); + results.add(Pair.of(PfCount, buildArgs("hll1", "hll2"))); transaction.pfmerge("hll", new String[] {"hll1", "hll2"}); + results.add(Pair.of(PfMerge, buildArgs("hll", "hll1", "hll2"))); + + transaction.sdiff(new String[] {"key1", "key2"}); + results.add(Pair.of(SDiff, buildArgs("key1", "key2"))); + + transaction.sdiffstore("key1", new String[] {"key2", "key3"}); + results.add(Pair.of(SDiffStore, buildArgs("key1", "key2", "key3"))); + + transaction.objectEncoding("key"); + results.add(Pair.of(ObjectEncoding, buildArgs("key"))); + + transaction.objectFreq("key"); + results.add(Pair.of(ObjectFreq, buildArgs("key"))); + + transaction.objectIdletime("key"); + results.add(Pair.of(ObjectIdleTime, buildArgs("key"))); + + transaction.objectRefcount("key"); + results.add(Pair.of(ObjectRefCount, buildArgs("key"))); + + transaction.touch(new String[] {"key1", "key2"}); + results.add(Pair.of(Touch, buildArgs("key1", "key2"))); + + transaction.geoadd("key", Map.of("Place", new GeospatialData(10.0, 20.0))); + results.add(Pair.of(GeoAdd, buildArgs("key", "10.0", "20.0", "Place"))); + + transaction.getbit("key", 1); + results.add(Pair.of(GetBit, buildArgs("key", "1"))); + + transaction.geoadd( + "key", + Map.of("Place", new GeospatialData(10.0, 20.0)), + new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS, true)); results.add( Pair.of( - PfMerge, - ArgsArray.newBuilder().addArgs("hll").addArgs("hll1").addArgs("hll2").build())); + GeoAdd, + buildArgs( + "key", + ConditionalChange.ONLY_IF_EXISTS.getValkeyApi(), + CHANGED_VALKEY_API, + "10.0", + "20.0", + "Place"))); + transaction.geopos("key", new String[] {"Place"}); + results.add(Pair.of(GeoPos, buildArgs("key", "Place"))); - transaction.sdiffstore("key1", new String[] {"key2", "key3"}); + transaction.functionLoad("pewpew", false).functionLoad("ololo", true); + results.add(Pair.of(FunctionLoad, buildArgs("pewpew"))); + results.add(Pair.of(FunctionLoad, buildArgs("REPLACE", "ololo"))); + + transaction.functionList(true).functionList("*", false); + results.add(Pair.of(FunctionList, buildArgs(WITH_CODE_VALKEY_API))); + results.add(Pair.of(FunctionList, buildArgs(LIBRARY_NAME_VALKEY_API, "*"))); + + transaction.functionDump(); + results.add(Pair.of(FunctionDump, buildArgs())); + + transaction.functionRestore("TEST".getBytes()); + results.add(Pair.of(FunctionRestore, buildArgs("TEST"))); + + transaction.fcall("func", new String[] {"key1", "key2"}, new String[] {"arg1", "arg2"}); + results.add(Pair.of(FCall, buildArgs("func", "2", "key1", "key2", "arg1", "arg2"))); + transaction.fcall("func", new String[] {"arg1", "arg2"}); + results.add(Pair.of(FCall, buildArgs("func", "0", "arg1", "arg2"))); + + transaction.fcallReadOnly("func", new String[] {"key1", "key2"}, new String[] {"arg1", "arg2"}); + results.add(Pair.of(FCallReadOnly, buildArgs("func", "2", "key1", "key2", "arg1", "arg2"))); + transaction.fcallReadOnly("func", new String[] {"arg1", "arg2"}); + results.add(Pair.of(FCallReadOnly, buildArgs("func", "0", "arg1", "arg2"))); + + transaction.functionStats(); + results.add(Pair.of(FunctionStats, buildArgs())); + + transaction.geodist("key", "Place", "Place2"); + results.add(Pair.of(GeoDist, buildArgs("key", "Place", "Place2"))); + transaction.geodist("key", "Place", "Place2", GeoUnit.KILOMETERS); + results.add(Pair.of(GeoDist, buildArgs("key", "Place", "Place2", "km"))); + + transaction.geohash("key", new String[] {"Place"}); + results.add(Pair.of(GeoHash, buildArgs("key", "Place"))); + + transaction.bitcount("key"); + results.add(Pair.of(BitCount, buildArgs("key"))); + + transaction.bitcount("key", 1, 1); + results.add(Pair.of(BitCount, buildArgs("key", "1", "1"))); + + transaction.bitcount("key", 1, 1, BitmapIndexType.BIT); + results.add(Pair.of(BitCount, buildArgs("key", "1", "1", BitmapIndexType.BIT.toString()))); + + transaction.setbit("key", 8, 1); + results.add(Pair.of(SetBit, buildArgs("key", "8", "1"))); + + transaction.bitpos("key", 1); + results.add(Pair.of(BitPos, buildArgs("key", "1"))); + transaction.bitpos("key", 0, 8); + results.add(Pair.of(BitPos, buildArgs("key", "0", "8"))); + transaction.bitpos("key", 1, 8, 10); + results.add(Pair.of(BitPos, buildArgs("key", "1", "8", "10"))); + transaction.bitpos("key", 1, 8, 10, BitmapIndexType.BIT); + results.add(Pair.of(BitPos, buildArgs("key", "1", "8", "10", BitmapIndexType.BIT.toString()))); + + transaction.bitop(BitwiseOperation.AND, "destination", new String[] {"key"}); + results.add(Pair.of(BitOp, buildArgs(BitwiseOperation.AND.toString(), "destination", "key"))); + + transaction.lmpop(new String[] {"key"}, ListDirection.LEFT); + results.add(Pair.of(LMPop, buildArgs("1", "key", "LEFT"))); + transaction.lmpop(new String[] {"key"}, ListDirection.LEFT, 1L); + results.add(Pair.of(LMPop, buildArgs("1", "key", "LEFT", "COUNT", "1"))); + + transaction.blmpop(new String[] {"key"}, ListDirection.LEFT, 0.1); + results.add(Pair.of(BLMPop, buildArgs("0.1", "1", "key", "LEFT"))); + transaction.blmpop(new String[] {"key"}, ListDirection.LEFT, 1L, 0.1); + results.add(Pair.of(BLMPop, buildArgs("0.1", "1", "key", "LEFT", "COUNT", "1"))); + + transaction.lset("key", 0, "zero"); + results.add(Pair.of(LSet, buildArgs("key", "0", "zero"))); + + transaction.lmove("key1", "key2", ListDirection.LEFT, ListDirection.LEFT); + results.add(Pair.of(LMove, buildArgs("key1", "key2", "LEFT", "LEFT"))); + + transaction.blmove("key1", "key2", ListDirection.LEFT, ListDirection.LEFT, 0.1); + results.add(Pair.of(BLMove, buildArgs("key1", "key2", "LEFT", "LEFT", "0.1"))); + + transaction.srandmember("key"); + results.add(Pair.of(SRandMember, buildArgs("key"))); + + transaction.srandmember("key", 1); + results.add(Pair.of(SRandMember, buildArgs("key", "1"))); + + transaction.spop("key"); + results.add(Pair.of(SPop, buildArgs("key"))); + + transaction.spopCount("key", 1); + results.add(Pair.of(SPop, buildArgs("key", "1"))); + + transaction.bitfieldReadOnly( + "key", + new BitFieldReadOnlySubCommands[] {new BitFieldGet(new SignedEncoding(5), new Offset(3))}); + results.add( + Pair.of( + BitFieldReadOnly, + buildArgs( + "key", + BitFieldOptions.GET_COMMAND_STRING, + BitFieldOptions.SIGNED_ENCODING_PREFIX.concat("5"), + "3"))); + transaction.bitfield( + "key", + new BitFieldSubCommands[] { + new BitFieldSet(new UnsignedEncoding(10), new OffsetMultiplier(3), 4) + }); + results.add( + Pair.of( + BitField, + buildArgs( + "key", + BitFieldOptions.SET_COMMAND_STRING, + BitFieldOptions.UNSIGNED_ENCODING_PREFIX.concat("10"), + BitFieldOptions.OFFSET_MULTIPLIER_PREFIX.concat("3"), + "4"))); + + transaction.sintercard(new String[] {"key1", "key2"}); + results.add(Pair.of(SInterCard, buildArgs("2", "key1", "key2"))); + + transaction.sintercard(new String[] {"key1", "key2"}, 1); + results.add(Pair.of(SInterCard, buildArgs("2", "key1", "key2", "LIMIT", "1"))); + + transaction.functionFlush().functionFlush(ASYNC); + results.add(Pair.of(FunctionFlush, buildArgs())); + results.add(Pair.of(FunctionFlush, buildArgs("ASYNC"))); + + transaction.functionDelete("LIB"); + results.add(Pair.of(FunctionDelete, buildArgs("LIB"))); + + transaction.copy("key1", "key2", true); + results.add(Pair.of(Copy, buildArgs("key1", "key2", REPLACE_VALKEY_API))); + + transaction.dump("key1"); + results.add(Pair.of(Dump, buildArgs("key1"))); + + transaction.restore("key2", 0, "TEST".getBytes()); + results.add(Pair.of(Restore, buildArgs("key2", "0", "TEST"))); + + transaction.lcs("key1", "key2"); + results.add(Pair.of(LCS, buildArgs("key1", "key2"))); + + transaction.lcsLen("key1", "key2"); + results.add(Pair.of(LCS, buildArgs("key1", "key2", "LEN"))); + + transaction.publish("msg", "ch1"); + results.add(Pair.of(Publish, buildArgs("ch1", "msg"))); + + transaction.lcsIdx("key1", "key2"); + results.add(Pair.of(LCS, buildArgs("key1", "key2", IDX_COMMAND_STRING))); + + transaction.lcsIdx("key1", "key2", 10); + results.add( + Pair.of( + LCS, buildArgs("key1", "key2", IDX_COMMAND_STRING, MINMATCHLEN_COMMAND_STRING, "10"))); + + transaction.lcsIdxWithMatchLen("key1", "key2"); + results.add( + Pair.of(LCS, buildArgs("key1", "key2", IDX_COMMAND_STRING, WITHMATCHLEN_COMMAND_STRING))); + + transaction.lcsIdxWithMatchLen("key1", "key2", 10); results.add( Pair.of( - SDiffStore, - ArgsArray.newBuilder().addArgs("key1").addArgs("key2").addArgs("key3").build())); + LCS, + buildArgs( + "key1", + "key2", + IDX_COMMAND_STRING, + MINMATCHLEN_COMMAND_STRING, + "10", + WITHMATCHLEN_COMMAND_STRING))); + + transaction.sunion(new String[] {"key1", "key2"}); + results.add(Pair.of(SUnion, buildArgs("key1", "key2"))); + + transaction.sort("key1"); + results.add(Pair.of(Sort, buildArgs("key1"))); + transaction.sortReadOnly("key1"); + results.add(Pair.of(SortReadOnly, buildArgs("key1"))); + transaction.sortStore("key1", "key2"); + results.add(Pair.of(Sort, buildArgs("key1", STORE_COMMAND_STRING, "key2"))); + + transaction.geosearch( + "key", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1, GeoUnit.KILOMETERS)); + results.add( + Pair.of( + GeoSearch, buildArgs("key", FROMMEMBER_VALKEY_API, "member", "BYRADIUS", "1.0", "km"))); + + transaction.geosearch( + "key", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1, 1, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC, 2)); + results.add( + Pair.of( + GeoSearch, + buildArgs( + "key", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYBOX", + "1.0", + "1.0", + "km", + COUNT_VALKEY_API, + "2", + "ASC"))); + + transaction.geosearch( + "key", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withhash().withdist().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 1, true)); + results.add( + Pair.of( + GeoSearch, + buildArgs( + "key", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km", + "WITHDIST", + "WITHCOORD", + "WITHHASH", + "COUNT", + "1", + "ANY", + "ASC"))); + + transaction.geosearch( + "key", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1, 1, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withhash().withdist().withcoord().build()); + results.add( + Pair.of( + GeoSearch, + buildArgs( + "key", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYBOX", + "1.0", + "1.0", + "km", + "WITHDIST", + "WITHCOORD", + "WITHHASH"))); + + transaction.geosearchstore( + "destination", + "source", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1, GeoUnit.KILOMETERS)); + results.add( + Pair.of( + GeoSearchStore, + buildArgs( + "destination", + "source", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km"))); + + transaction.geosearchstore( + "destination", + "source", + new GeoSearchOrigin.CoordOrigin(new GeospatialData(1.0, 1.0)), + new GeoSearchShape(1, 1, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC, 2)); + results.add( + Pair.of( + GeoSearchStore, + buildArgs( + "destination", + "source", + FROMLONLAT_VALKEY_API, + "1.0", + "1.0", + "BYBOX", + "1.0", + "1.0", + "km", + COUNT_VALKEY_API, + "2", + "ASC"))); + + transaction.geosearchstore( + "destination", + "source", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build()); + results.add( + Pair.of( + GeoSearchStore, + buildArgs( + "destination", + "source", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km", + "STOREDIST"))); + + transaction.geosearchstore( + "destination", + "source", + new GeoSearchOrigin.MemberOrigin("member"), + new GeoSearchShape(1, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build(), + new GeoSearchResultOptions(SortOrder.ASC, 1, true)); + results.add( + Pair.of( + GeoSearchStore, + buildArgs( + "destination", + "source", + FROMMEMBER_VALKEY_API, + "member", + "BYRADIUS", + "1.0", + "km", + "STOREDIST", + "COUNT", + "1", + "ANY", + "ASC"))); + + transaction.sscan("key1", "0"); + results.add(Pair.of(SScan, buildArgs("key1", "0"))); + + transaction.sscan("key1", "0", SScanOptions.builder().matchPattern("*").count(10L).build()); + results.add( + Pair.of( + SScan, + buildArgs( + "key1", + "0", + SScanOptions.MATCH_OPTION_STRING, + "*", + SScanOptions.COUNT_OPTION_STRING, + "10"))); + + transaction.zscan("key1", "0"); + results.add(Pair.of(ZScan, buildArgs("key1", "0"))); + + transaction.zscan("key1", "0", ZScanOptions.builder().matchPattern("*").count(10L).build()); + results.add( + Pair.of( + ZScan, + buildArgs( + "key1", + "0", + ZScanOptions.MATCH_OPTION_STRING, + "*", + ZScanOptions.COUNT_OPTION_STRING, + "10"))); + + transaction.hscan("key1", "0"); + results.add(Pair.of(HScan, buildArgs("key1", "0"))); + + transaction.hscan("key1", "0", HScanOptions.builder().matchPattern("*").count(10L).build()); + results.add( + Pair.of( + HScan, + buildArgs( + "key1", + "0", + HScanOptions.MATCH_OPTION_STRING, + "*", + HScanOptions.COUNT_OPTION_STRING, + "10"))); + + transaction.wait(1L, 1000L); + results.add(Pair.of(Wait, buildArgs("1", "1000"))); var protobufTransaction = transaction.getProtobufTransaction().build(); @@ -520,10 +1520,10 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), } } - private ArgsArray buildArgs(String... args) { + static ArgsArray buildArgs(String... args) { var builder = ArgsArray.newBuilder(); for (var arg : args) { - builder.addArgs(arg); + builder.addArgs(ByteString.copyFromUtf8(arg)); } return builder.build(); } diff --git a/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java b/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java index 08235ac1fc..4edc43caaf 100644 --- a/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java +++ b/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connection; import static java.util.concurrent.TimeUnit.SECONDS; @@ -8,9 +8,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import command_request.CommandRequestOuterClass.CommandRequest; import connection_request.ConnectionRequestOuterClass.ConnectionRequest; import connection_request.ConnectionRequestOuterClass.NodeAddress; -import glide.api.RedisClient; +import glide.api.GlideClient; +import glide.api.logging.Logger; import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; @@ -27,7 +29,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import redis_request.RedisRequestOuterClass.RedisRequest; import response.ResponseOuterClass.Response; public class ConnectionWithGlideMockTests extends RustCoreLibMockTestBase { @@ -37,9 +38,13 @@ public class ConnectionWithGlideMockTests extends RustCoreLibMockTestBase { @BeforeEach @SneakyThrows public void createTestClient() { + // TODO: Add DISABLED level to logger-core + Logger.setLoggerConfig(Logger.Level.DISABLED); channelHandler = new ChannelHandler( - new CallbackDispatcher(), socketPath, Platform.getThreadPoolResourceSupplier().get()); + new CallbackDispatcher(null), + socketPath, + Platform.getThreadPoolResourceSupplier().get()); } @AfterEach @@ -64,7 +69,7 @@ public static void init() { @Test @SneakyThrows - // as of #710 https://github.com/aws/babushka/pull/710 - connection response is empty + // as of #710 https://github.com/valkey-io/valkey-glide/pull/710 - connection response is empty public void can_connect_with_empty_response() { RustCoreMock.updateGlideMock( new RustCoreMock.GlideMockProtobuf() { @@ -74,7 +79,7 @@ public Response connection(ConnectionRequest request) { } @Override - public Response.Builder redisRequest(RedisRequest request) { + public Response.Builder commandRequest(CommandRequest request) { return null; } }); @@ -97,7 +102,7 @@ public Response connection(ConnectionRequest request) { } @Override - public Response.Builder redisRequest(RedisRequest request) { + public Response.Builder commandRequest(CommandRequest request) { return null; } }); @@ -120,7 +125,7 @@ public Response connection(ConnectionRequest request) { } @Override - public Response.Builder redisRequest(RedisRequest request) { + public Response.Builder commandRequest(CommandRequest request) { return null; } }); @@ -139,7 +144,7 @@ public Response connection(ConnectionRequest request) { } @Override - public Response.Builder redisRequest(RedisRequest request) { + public Response.Builder commandRequest(CommandRequest request) { return null; } }); @@ -182,10 +187,15 @@ public void rethrow_error_if_UDS_channel_closed() { } } - private static class TestClient extends RedisClient { + private static class TestClient extends GlideClient { public TestClient(ChannelHandler channelHandler) { - super(new ConnectionManager(channelHandler), new CommandManager(channelHandler)); + super( + new ClientBuilder( + new ConnectionManager(channelHandler), + new CommandManager(channelHandler), + null, + null)); } } } diff --git a/java/client/src/test/java/glide/connectors/handlers/MessageHandlerTests.java b/java/client/src/test/java/glide/connectors/handlers/MessageHandlerTests.java new file mode 100644 index 0000000000..ae6c71302c --- /dev/null +++ b/java/client/src/test/java/glide/connectors/handlers/MessageHandlerTests.java @@ -0,0 +1,234 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.connectors.handlers; + +import static glide.api.models.GlideString.gs; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import glide.api.models.PubSubMessage; +import glide.api.models.configuration.BaseSubscriptionConfiguration; +import glide.api.models.exceptions.GlideException; +import glide.managers.BaseResponseResolver; +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import response.ResponseOuterClass; + +/** Unit tests for MessageHandler */ +public class MessageHandlerTests { + @Test + @SneakyThrows + public void test_exact_message() { + // Arrange. + // TODO: move fakeResolver creation into a test method + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.Message, + "values", + new byte[][] {gs("channel").getBytes(), gs("message").getBytes()}); + } + }; + MessageHandler handler = new MessageHandler(Optional.empty(), Optional.empty(), fakeResolver); + + // Act. + handler.handle(null); + + // Assert. + PubSubMessage expected = new PubSubMessage(gs("message"), gs("channel")); + assertEquals(expected, handler.getQueue().popSync()); + } + + @Test + @SneakyThrows + public void test_exact_message_with_callback() { + // Arrange. + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.Message, + "values", + new byte[][] {gs("channel").getBytes(), gs("message").getBytes()}); + } + }; + ArrayList messageList = new ArrayList<>(); + BaseSubscriptionConfiguration.MessageCallback callback = + (message, context) -> { + ArrayList contextAsList = (ArrayList) context; + contextAsList.add(message); + }; + + MessageHandler handler = + new MessageHandler(Optional.of(callback), Optional.of(messageList), fakeResolver); + + // Act. + handler.handle(null); + + // Assert. + PubSubMessage expected = new PubSubMessage(gs("message"), gs("channel")); + assertEquals(1, messageList.size()); + assertEquals(expected, messageList.get(0)); + } + + @Test + @SneakyThrows + public void test_sharded_message() { + // Arrange. + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.SMessage, + "values", + new byte[][] {gs("channel").getBytes(), gs("message").getBytes()}); + } + }; + MessageHandler handler = new MessageHandler(Optional.empty(), Optional.empty(), fakeResolver); + + // Act. + handler.handle(null); + + // Assert. + PubSubMessage expected = new PubSubMessage(gs("message"), gs("channel")); + assertEquals(expected, handler.getQueue().popSync()); + } + + @Test + public void test_sharded_message_with_callback() throws Exception { + // Arrange. + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.SMessage, + "values", + new byte[][] {gs("channel").getBytes(), gs("message").getBytes()}); + } + }; + ArrayList messageList = new ArrayList<>(); + BaseSubscriptionConfiguration.MessageCallback callback = + (message, context) -> { + ArrayList contextAsList = (ArrayList) context; + contextAsList.add(message); + }; + + MessageHandler handler = + new MessageHandler(Optional.of(callback), Optional.of(messageList), fakeResolver); + + // Act. + handler.handle(null); + + // Assert. + PubSubMessage expected = new PubSubMessage(gs("message"), gs("channel")); + assertEquals(1, messageList.size()); + assertEquals(expected, messageList.get(0)); + } + + @Test + @SneakyThrows + public void test_pattern_message() { + // Arrange. + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.PMessage, + "values", + new byte[][] { + gs("pattern").getBytes(), gs("channel").getBytes(), gs("message").getBytes() + }); + } + }; + MessageHandler handler = new MessageHandler(Optional.empty(), Optional.empty(), fakeResolver); + + // Act. + handler.handle(null); + + // Assert. + PubSubMessage expected = new PubSubMessage(gs("message"), gs("channel"), gs("pattern")); + assertEquals(expected, handler.getQueue().popSync()); + } + + @Test + @SneakyThrows + public void test_pattern_message_with_callback() { + // Arrange. + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.PMessage, + "values", + new byte[][] { + gs("pattern").getBytes(), gs("channel").getBytes(), gs("message").getBytes() + }); + } + }; + ArrayList messageList = new ArrayList<>(); + BaseSubscriptionConfiguration.MessageCallback callback = + (message, context) -> { + ArrayList contextAsList = (ArrayList) context; + contextAsList.add(message); + }; + + MessageHandler handler = + new MessageHandler(Optional.of(callback), Optional.of(messageList), fakeResolver); + + // Act. + handler.handle(null); + + // Assert. + PubSubMessage expected = new PubSubMessage(gs("message"), gs("channel"), gs("pattern")); + assertEquals(1, messageList.size()); + assertEquals(expected, messageList.get(0)); + } + + @Test + public void test_exception_from_callback_wrapped() throws Exception { + // Arrange. + BaseResponseResolver fakeResolver = + new BaseResponseResolver(null) { + @Override + public Object apply(ResponseOuterClass.Response response) throws GlideException { + return Map.of( + "kind", + MessageHandler.PushKind.Message, + "values", + new byte[][] {gs("channel").getBytes(), gs("message").getBytes()}); + } + }; + ArrayList messageList = new ArrayList<>(); + BaseSubscriptionConfiguration.MessageCallback callback = + (message, context) -> { + throw new RuntimeException(message.getMessage().getString()); + }; + + MessageHandler handler = + new MessageHandler(Optional.of(callback), Optional.of(messageList), fakeResolver); + + // Act. + MessageHandler.MessageCallbackException ex = + assertThrows(MessageHandler.MessageCallbackException.class, () -> handler.handle(null)); + + // Assert. + assertInstanceOf(RuntimeException.class, ex.getCause()); + assertEquals(new RuntimeException("message").getMessage(), ex.getCause().getMessage()); + } +} diff --git a/java/client/src/test/java/glide/connectors/handlers/PubSubMessageQueueTests.java b/java/client/src/test/java/glide/connectors/handlers/PubSubMessageQueueTests.java new file mode 100644 index 0000000000..37339f61a3 --- /dev/null +++ b/java/client/src/test/java/glide/connectors/handlers/PubSubMessageQueueTests.java @@ -0,0 +1,273 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.connectors.handlers; + +import static glide.api.models.GlideString.gs; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.api.models.PubSubMessage; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +@Timeout(30) // sec +public class PubSubMessageQueueTests { + + private void checkFutureStatus(CompletableFuture future, boolean shouldBeDone) { + assertEquals(shouldBeDone, future.isDone()); + assertFalse(future.isCancelled()); + assertFalse(future.isCompletedExceptionally()); + } + + @Test + @SneakyThrows + public void async_read_messages_then_add() { + var queue = new MessageHandler.PubSubMessageQueue(); + + // read async - receiving promises + var promise1 = queue.popAsync(); + var promise2 = queue.popAsync(); + + // async reading from empty queue returns the same future for the same message + assertSame(promise1, promise2); + checkFutureStatus(promise1, false); + assertTrue(queue.messageQueue.isEmpty()); + + // now - add + var msg1 = new PubSubMessage(gs("one"), gs("one")); + var msg2 = new PubSubMessage(gs("two"), gs("two")); + var msg3 = new PubSubMessage(gs("three"), gs("three")); + queue.push(msg1); + queue.push(msg2); + queue.push(msg3); + + // promises should get resolved automagically + checkFutureStatus(promise1, true); + assertSame(msg1, promise1.get()); + // `firstMessagePromise` is a new uncompleted future + checkFutureStatus(queue.firstMessagePromise, false); + // and `msg1` isn't stored in the Q + assertEquals(2, queue.messageQueue.size()); + assertSame(msg2, queue.messageQueue.pop()); + assertSame(msg3, queue.messageQueue.pop()); + // now MQ should be empty + assertTrue(queue.messageQueue.isEmpty()); + } + + @Test + @SneakyThrows + public void sync_read_messages_then_add() { + var queue = new MessageHandler.PubSubMessageQueue(); + + // read async - receiving nulls + assertNull(queue.popSync()); + assertNull(queue.popSync()); + + // `firstMessagePromise` remains unset and unused + checkFutureStatus(queue.firstMessagePromise, false); + // and Q is empty + assertTrue(queue.messageQueue.isEmpty()); + + // now - add + var msg1 = new PubSubMessage(gs("one"), gs("one")); + var msg2 = new PubSubMessage(gs("two"), gs("two")); + var msg3 = new PubSubMessage(gs("three"), gs("three")); + queue.push(msg1); + queue.push(msg2); + queue.push(msg3); + + // `firstMessagePromise` remains unset and unused + checkFutureStatus(queue.firstMessagePromise, false); + // all 3 messages are stored in the Q + assertEquals(3, queue.messageQueue.size()); + + // reading them + assertSame(msg1, queue.popSync()); + assertSame(msg2, queue.popSync()); + assertSame(msg3, queue.popSync()); + } + + @Test + @SneakyThrows + public void add_messages_then_read() { + var queue = new MessageHandler.PubSubMessageQueue(); + + var msg1 = new PubSubMessage(gs("one"), gs("one")); + var msg2 = new PubSubMessage(gs("two"), gs("two")); + var msg3 = new PubSubMessage(gs("three"), gs("three")); + var msg4 = new PubSubMessage(gs("four"), gs("four")); + queue.push(msg1); + queue.push(msg2); + queue.push(msg3); + queue.push(msg4); + + // `firstMessagePromise` remains unset and unused + checkFutureStatus(queue.firstMessagePromise, false); + // all messages are stored in the Q + assertEquals(4, queue.messageQueue.size()); + + // now - read one async + assertSame(msg1, queue.popAsync().get()); + // `firstMessagePromise` remains unset and unused + checkFutureStatus(queue.firstMessagePromise, false); + // Q stores remaining 3 messages + assertEquals(3, queue.messageQueue.size()); + + // read sync + assertSame(msg2, queue.popSync()); + checkFutureStatus(queue.firstMessagePromise, false); + assertEquals(2, queue.messageQueue.size()); + + // keep reading + // get a future for the next message + var future = queue.popAsync(); + checkFutureStatus(future, true); + checkFutureStatus(queue.firstMessagePromise, false); + assertEquals(1, queue.messageQueue.size()); + // then read sync + assertSame(msg4, queue.popSync()); + // nothing remains in the Q + assertEquals(0, queue.messageQueue.size()); + // message 3 isn't lost - it is stored in `future` + assertSame(msg3, future.get()); + } + + @Test + @SneakyThrows + public void getting_messages_reordered_on_concurrent_async_and_sync_read() { + var queue = new MessageHandler.PubSubMessageQueue(); + var msg1 = new PubSubMessage(gs("one"), gs("one")); + var msg2 = new PubSubMessage(gs("two"), gs("two")); + queue.push(msg1); + queue.push(msg2); + + var readMessages = new ArrayList(2); + + // assuming thread 1 started async read + var future = queue.popAsync(); + // and got raced by thread 2 which reads sync + var msg = queue.popSync(); + readMessages.add(msg); + // then thread 1 continues + msg = future.get(); + readMessages.add(msg); + + // messages get reordered since stored into a single collection (even if is a concurrent one) + assertEquals(List.of(msg2, msg1), readMessages); + + // another example + + // reading async before anything added to the queue + future = queue.popAsync(); + // queue gets 2 messages + queue.push(msg1); + queue.push(msg2); + // but inside the queue only one is stored + assertEquals(1, queue.messageQueue.size()); + // then if we read sync, we receive only second one + assertSame(msg2, queue.popSync()); + // future gets resolved by the first message + assertSame(msg1, future.get()); + } + + // Not merging `concurrent_write_async_read` and `concurrent_write_sync_read`, because + // concurrent sync and async read may reorder messages + + @Test + @SneakyThrows + public void concurrent_write_async_read() { + var queue = new MessageHandler.PubSubMessageQueue(); + var numMessages = 1000; // test takes ~0.5 sec + // collections aren't concurrent, since we have only 1 reader and 1 writer so far + var expected = new LinkedList(); + var actual = new LinkedList(); + var rand = new Random(); + for (int i = 0; i < numMessages; i++) { + expected.add( + new PubSubMessage(gs(i + " " + UUID.randomUUID()), gs(UUID.randomUUID().toString()))); + } + + Runnable writer = + () -> { + for (var message : expected) { + queue.push(message); + try { + Thread.sleep(rand.nextInt(2)); + } catch (InterruptedException ignored) { + } + } + }; + Runnable reader = + () -> { + do { + try { + var message = queue.popAsync().get(); + actual.add(message); + Thread.sleep(rand.nextInt(2)); + } catch (Exception ignored) { + } + } while (actual.size() < expected.size()); + }; + + // start reader and writer and wait for finish + CompletableFuture.allOf(CompletableFuture.runAsync(writer), CompletableFuture.runAsync(reader)) + .get(); + + // this verifies message order + assertEquals(expected, actual); + } + + @Test + @SneakyThrows + public void concurrent_write_sync_read() { + var queue = new MessageHandler.PubSubMessageQueue(); + var numMessages = 1000; // test takes ~0.5 sec + // collections aren't concurrent, since we have only 1 reader and 1 writer so far + var expected = new LinkedList(); + var actual = new LinkedList(); + var rand = new Random(); + for (int i = 0; i < numMessages; i++) { + expected.add( + new PubSubMessage(gs(i + " " + UUID.randomUUID()), gs(UUID.randomUUID().toString()))); + } + + Runnable writer = + () -> { + for (var message : expected) { + queue.push(message); + try { + Thread.sleep(rand.nextInt(2)); + } catch (InterruptedException ignored) { + } + } + }; + Runnable reader = + () -> { + do { + try { + var message = queue.popSync(); + if (message != null) { + actual.add(message); + } + Thread.sleep(rand.nextInt(2)); + } catch (InterruptedException ignored) { + } + } while (actual.size() < expected.size()); + }; + // start reader and writer and wait for finish + CompletableFuture.allOf(CompletableFuture.runAsync(writer), CompletableFuture.runAsync(reader)) + .get(); + + // this verifies message order + assertEquals(expected, actual); + } +} diff --git a/java/client/src/test/java/glide/connectors/resources/ThreadPoolResourceAllocatorTest.java b/java/client/src/test/java/glide/connectors/resources/ThreadPoolResourceAllocatorTest.java index 3ce2052582..15d195deef 100644 --- a/java/client/src/test/java/glide/connectors/resources/ThreadPoolResourceAllocatorTest.java +++ b/java/client/src/test/java/glide/connectors/resources/ThreadPoolResourceAllocatorTest.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.resources; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/java/client/src/test/java/glide/ffi/FfiTest.java b/java/client/src/test/java/glide/ffi/FfiTest.java index 73c9082c20..d5e33e7736 100644 --- a/java/client/src/test/java/glide/ffi/FfiTest.java +++ b/java/client/src/test/java/glide/ffi/FfiTest.java @@ -1,15 +1,18 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.ffi; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import glide.ffi.resolvers.RedisValueResolver; +import glide.ffi.resolvers.GlideValueResolver; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; +import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -42,10 +45,22 @@ public class FfiTest { public static native long createLeakedLongSet(long[] value); + // This tests that panics do not cross the FFI boundary and an exception is thrown if a panic is + // caught + public static native long handlePanics( + boolean shouldPanic, boolean errorPresent, long value, long defaultValue); + + // This tests that Rust errors are properly converted into Java exceptions and thrown + public static native long handleErrors(boolean isSuccess, long value, long defaultValue); + + // This tests that a Java exception is properly thrown across the FFI boundary + public static native void throwException( + boolean throwTwice, boolean isRuntimeException, String message); + @Test public void redisValueToJavaValue_Nil() { long ptr = FfiTest.createLeakedNil(); - Object nilValue = RedisValueResolver.valueFromPointer(ptr); + Object nilValue = GlideValueResolver.valueFromPointer(ptr); assertNull(nilValue); } @@ -53,14 +68,14 @@ public void redisValueToJavaValue_Nil() { @ValueSource(strings = {"hello", "cat", "dog"}) public void redisValueToJavaValue_SimpleString(String input) { long ptr = FfiTest.createLeakedSimpleString(input); - Object simpleStringValue = RedisValueResolver.valueFromPointer(ptr); + Object simpleStringValue = GlideValueResolver.valueFromPointer(ptr); assertEquals(input, simpleStringValue); } @Test public void redisValueToJavaValue_Okay() { long ptr = FfiTest.createLeakedOkay(); - Object okayValue = RedisValueResolver.valueFromPointer(ptr); + Object okayValue = GlideValueResolver.valueFromPointer(ptr); assertEquals("OK", okayValue); } @@ -68,17 +83,24 @@ public void redisValueToJavaValue_Okay() { @ValueSource(longs = {0L, 100L, 774L, Integer.MAX_VALUE + 1L, Integer.MIN_VALUE - 1L}) public void redisValueToJavaValue_Int(Long input) { long ptr = FfiTest.createLeakedInt(input); - Object longValue = RedisValueResolver.valueFromPointer(ptr); + Object longValue = GlideValueResolver.valueFromPointer(ptr); assertTrue(longValue instanceof Long); assertEquals(input, longValue); } @Test + @SneakyThrows public void redisValueToJavaValue_BulkString() { - String input = "😀\n💎\n🗿"; - byte[] bulkString = input.getBytes(); + // This is explicitly for testing non-ASCII UTF-8 byte sequences. + // Note that these can't be encoded as String literals without introducing compiler + // warnings and errors. + + // This is the 'alpha' character. + byte[] bulkString = new byte[] {(byte) 0xCE, (byte) 0xB1}; long ptr = FfiTest.createLeakedBulkString(bulkString); - Object bulkStringValue = RedisValueResolver.valueFromPointer(ptr); + final String input; + input = new String(bulkString, StandardCharsets.UTF_8); + Object bulkStringValue = GlideValueResolver.valueFromPointer(ptr); assertEquals(input, bulkStringValue); } @@ -86,7 +108,7 @@ public void redisValueToJavaValue_BulkString() { public void redisValueToJavaValue_Array() { long[] array = {1L, 2L, 3L}; long ptr = FfiTest.createLeakedLongArray(array); - Object longArrayValue = RedisValueResolver.valueFromPointer(ptr); + Object longArrayValue = GlideValueResolver.valueFromPointer(ptr); assertTrue(longArrayValue instanceof Object[]); Object[] result = (Object[]) longArrayValue; assertArrayEquals(new Object[] {1L, 2L, 3L}, result); @@ -97,7 +119,7 @@ public void redisValueToJavaValue_Map() { long[] keys = {12L, 14L, 23L}; long[] values = {1L, 2L, 3L}; long ptr = FfiTest.createLeakedMap(keys, values); - Object mapValue = RedisValueResolver.valueFromPointer(ptr); + Object mapValue = GlideValueResolver.valueFromPointer(ptr); assertTrue(mapValue instanceof HashMap); HashMap result = (HashMap) mapValue; assertAll( @@ -110,14 +132,14 @@ public void redisValueToJavaValue_Map() { @ValueSource(doubles = {1.0d, 25.2d, 103.5d}) public void redisValueToJavaValue_Double(Double input) { long ptr = FfiTest.createLeakedDouble(input); - Object doubleValue = RedisValueResolver.valueFromPointer(ptr); + Object doubleValue = GlideValueResolver.valueFromPointer(ptr); assertEquals(input, doubleValue); } @Test public void redisValueToJavaValue_Boolean() { long ptr = FfiTest.createLeakedBoolean(true); - Object booleanValue = RedisValueResolver.valueFromPointer(ptr); + Object booleanValue = GlideValueResolver.valueFromPointer(ptr); assertTrue((Boolean) booleanValue); } @@ -125,7 +147,7 @@ public void redisValueToJavaValue_Boolean() { @ValueSource(strings = {"hello", "cat", "dog"}) public void redisValueToJavaValue_VerbatimString(String input) { long ptr = FfiTest.createLeakedVerbatimString(input); - Object verbatimStringValue = RedisValueResolver.valueFromPointer(ptr); + Object verbatimStringValue = GlideValueResolver.valueFromPointer(ptr); assertEquals(input, verbatimStringValue); } @@ -133,7 +155,7 @@ public void redisValueToJavaValue_VerbatimString(String input) { public void redisValueToJavaValue_Set() { long[] array = {1L, 2L, 2L}; long ptr = FfiTest.createLeakedLongSet(array); - Object longSetValue = RedisValueResolver.valueFromPointer(ptr); + Object longSetValue = GlideValueResolver.valueFromPointer(ptr); assertTrue(longSetValue instanceof HashSet); HashSet result = (HashSet) longSetValue; assertAll( @@ -141,4 +163,52 @@ public void redisValueToJavaValue_Set() { () -> assertTrue(result.contains(2L)), () -> assertEquals(result.size(), 2)); } + + @Test + public void handlePanics_panic() { + long expectedValue = 0L; + long value = FfiTest.handlePanics(true, false, 1L, expectedValue); + assertEquals(expectedValue, value); + } + + @Test + public void handlePanics_returnError() { + long expectedValue = 0L; + long value = FfiTest.handlePanics(false, true, 1L, expectedValue); + assertEquals(expectedValue, value); + } + + @Test + public void handlePanics_returnValue() { + long expectedValue = 2L; + long value = FfiTest.handlePanics(false, false, expectedValue, 0L); + assertEquals(expectedValue, value); + } + + @Test + public void handleErrors_success() { + long expectedValue = 0L; + long value = FfiTest.handleErrors(true, expectedValue, 1L); + assertEquals(expectedValue, value); + } + + @Test + public void handleErrors_error() { + assertThrows(Exception.class, () -> FfiTest.handleErrors(false, 0L, 1L)); + } + + @Test + public void throwException() { + assertThrows(Exception.class, () -> FfiTest.throwException(false, false, "My message")); + } + + @Test + public void throwException_throwTwice() { + assertThrows(Exception.class, () -> FfiTest.throwException(true, false, "My message")); + } + + @Test + public void throwException_throwRuntimeException() { + assertThrows(RuntimeException.class, () -> FfiTest.throwException(false, true, "My message")); + } } diff --git a/java/client/src/test/java/glide/managers/CommandManagerTest.java b/java/client/src/test/java/glide/managers/CommandManagerTest.java index b6e8d01349..418377ccd9 100644 --- a/java/client/src/test/java/glide/managers/CommandManagerTest.java +++ b/java/client/src/test/java/glide/managers/CommandManagerTest.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; +import static command_request.CommandRequestOuterClass.RequestType.CustomCommand; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; @@ -15,8 +16,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; +import com.google.protobuf.ByteString; +import command_request.CommandRequestOuterClass.CommandRequest; +import command_request.CommandRequestOuterClass.SimpleRoutes; +import command_request.CommandRequestOuterClass.SlotTypes; import glide.api.models.ClusterTransaction; import glide.api.models.Transaction; import glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; @@ -39,9 +43,6 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; -import redis_request.RedisRequestOuterClass.RedisRequest; -import redis_request.RedisRequestOuterClass.SimpleRoutes; -import redis_request.RedisRequestOuterClass.SlotTypes; import response.ResponseOuterClass.Response; public class CommandManagerTest { @@ -75,7 +76,7 @@ public void submitNewCommand_return_Object_result() { service.submitNewCommand( CustomCommand, new String[0], - new BaseCommandResponseResolver((ptr) -> ptr == pointer ? respObject : null)); + new BaseResponseResolver((ptr) -> ptr == pointer ? respObject : null)); Object respPointer = result.get(); // verify @@ -97,7 +98,9 @@ public void submitNewCommand_return_Null_result() { service.submitNewCommand( CustomCommand, new String[0], - new BaseCommandResponseResolver((p) -> new RuntimeException(""))); + new BaseResponseResolver( + (p) -> + new RuntimeException("Testing: something went wrong if you see this error"))); Object respPointer = result.get(); // verify @@ -124,7 +127,7 @@ public void submitNewCommand_return_String_result() { service.submitNewCommand( CustomCommand, new String[0], - new BaseCommandResponseResolver((p) -> p == pointer ? testString : null)); + new BaseResponseResolver((p) -> p == pointer ? testString : null)); Object respPointer = result.get(); // verify @@ -143,8 +146,8 @@ public void prepare_request_with_simple_routes(Route routeType) { when(channelHandler.write(any(), anyBoolean())).thenReturn(future); when(channelHandler.isClosed()).thenReturn(false); - ArgumentCaptor captor = - ArgumentCaptor.forClass(RedisRequest.Builder.class); + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommandRequest.Builder.class); service.submitNewCommand(CustomCommand, new String[0], routeType, r -> null); verify(channelHandler).write(captor.capture(), anyBoolean()); @@ -174,8 +177,8 @@ public void prepare_request_with_slot_id_routes(SlotType slotType) { when(channelHandler.write(any(), anyBoolean())).thenReturn(future); when(channelHandler.isClosed()).thenReturn(false); - ArgumentCaptor captor = - ArgumentCaptor.forClass(RedisRequest.Builder.class); + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommandRequest.Builder.class); service.submitNewCommand( CustomCommand, new String[0], new SlotIdRoute(42, slotType), r -> null); @@ -207,8 +210,8 @@ public void prepare_request_with_slot_key_routes(SlotType slotType) { when(channelHandler.write(any(), anyBoolean())).thenReturn(future); when(channelHandler.isClosed()).thenReturn(false); - ArgumentCaptor captor = - ArgumentCaptor.forClass(RedisRequest.Builder.class); + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommandRequest.Builder.class); service.submitNewCommand( CustomCommand, new String[0], new SlotKeyRoute("TEST", slotType), r -> null); @@ -239,8 +242,8 @@ public void prepare_request_with_by_address_route() { when(channelHandler.write(any(), anyBoolean())).thenReturn(future); when(channelHandler.isClosed()).thenReturn(false); - ArgumentCaptor captor = - ArgumentCaptor.forClass(RedisRequest.Builder.class); + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommandRequest.Builder.class); service.submitNewCommand( CustomCommand, new String[0], new ByAddressRoute("testhost", 6379), r -> null); @@ -284,11 +287,11 @@ public void submitNewCommand_with_Transaction_sends_protobuf_request() { when(channelHandler.write(any(), anyBoolean())).thenReturn(future); when(channelHandler.isClosed()).thenReturn(false); - ArgumentCaptor captor = - ArgumentCaptor.forClass(RedisRequest.Builder.class); + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommandRequest.Builder.class); // exercise - service.submitNewCommand(trans, r -> null); + service.submitNewTransaction(trans, r -> null); // verify verify(channelHandler).write(captor.capture(), anyBoolean()); @@ -298,14 +301,14 @@ public void submitNewCommand_with_Transaction_sends_protobuf_request() { assertTrue(requestBuilder.hasTransaction()); assertEquals(3, requestBuilder.getTransaction().getCommandsCount()); - LinkedList resultPayloads = new LinkedList<>(); - resultPayloads.add("one"); - resultPayloads.add("two"); - resultPayloads.add("three"); - for (redis_request.RedisRequestOuterClass.Command command : + LinkedList resultPayloads = new LinkedList<>(); + resultPayloads.add(ByteString.copyFromUtf8("one")); + resultPayloads.add(ByteString.copyFromUtf8("two")); + resultPayloads.add(ByteString.copyFromUtf8("three")); + for (command_request.CommandRequestOuterClass.Command command : requestBuilder.getTransaction().getCommandsList()) { assertEquals(CustomCommand, command.getRequestType()); - assertEquals("GETSTRING", command.getArgsArray().getArgs(0)); + assertEquals(ByteString.copyFromUtf8("GETSTRING"), command.getArgsArray().getArgs(0)); assertEquals(resultPayloads.pop(), command.getArgsArray().getArgs(1)); } } @@ -325,10 +328,10 @@ public void submitNewCommand_with_ClusterTransaction_with_route_sends_protobuf_r when(channelHandler.write(any(), anyBoolean())).thenReturn(future); when(channelHandler.isClosed()).thenReturn(false); - ArgumentCaptor captor = - ArgumentCaptor.forClass(RedisRequest.Builder.class); + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommandRequest.Builder.class); - service.submitNewCommand(trans, Optional.of(routeType), r -> null); + service.submitNewTransaction(trans, Optional.of(routeType), r -> null); verify(channelHandler).write(captor.capture(), anyBoolean()); var requestBuilder = captor.getValue(); diff --git a/java/client/src/test/java/glide/managers/ConnectionManagerTest.java b/java/client/src/test/java/glide/managers/ConnectionManagerTest.java index 79389fcde1..9a3ebe6e19 100644 --- a/java/client/src/test/java/glide/managers/ConnectionManagerTest.java +++ b/java/client/src/test/java/glide/managers/ConnectionManagerTest.java @@ -1,8 +1,11 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.managers; +import static glide.api.models.GlideString.gs; import static glide.api.models.configuration.NodeAddress.DEFAULT_HOST; import static glide.api.models.configuration.NodeAddress.DEFAULT_PORT; +import static glide.api.models.configuration.StandaloneSubscriptionConfiguration.PubSubChannelMode.EXACT; +import static glide.api.models.configuration.StandaloneSubscriptionConfiguration.PubSubChannelMode.PATTERN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -13,20 +16,25 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.protobuf.ByteString; import connection_request.ConnectionRequestOuterClass; import connection_request.ConnectionRequestOuterClass.AuthenticationInfo; import connection_request.ConnectionRequestOuterClass.ConnectionRequest; import connection_request.ConnectionRequestOuterClass.ConnectionRetryStrategy; +import connection_request.ConnectionRequestOuterClass.PubSubChannelsOrPatterns; +import connection_request.ConnectionRequestOuterClass.PubSubSubscriptions; import connection_request.ConnectionRequestOuterClass.TlsMode; import glide.api.models.configuration.BackoffStrategy; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.ReadFrom; -import glide.api.models.configuration.RedisClientConfiguration; -import glide.api.models.configuration.RedisClusterClientConfiguration; -import glide.api.models.configuration.RedisCredentials; +import glide.api.models.configuration.ServerCredentials; +import glide.api.models.configuration.StandaloneSubscriptionConfiguration; import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.ChannelHandler; import io.netty.channel.ChannelFuture; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; @@ -69,7 +77,7 @@ public void setUp() { @Test public void connection_request_protobuf_generation_default_standalone_configuration() { // setup - RedisClientConfiguration redisClientConfiguration = RedisClientConfiguration.builder().build(); + GlideClientConfiguration glideClientConfiguration = GlideClientConfiguration.builder().build(); ConnectionRequest expectedProtobufConnectionRequest = ConnectionRequest.newBuilder() .setTlsMode(TlsMode.NoTls) @@ -82,7 +90,7 @@ public void connection_request_protobuf_generation_default_standalone_configurat // execute when(channel.connect(eq(expectedProtobufConnectionRequest))).thenReturn(completedFuture); - CompletableFuture result = connectionManager.connectToRedis(redisClientConfiguration); + CompletableFuture result = connectionManager.connectToValkey(glideClientConfiguration); // verify // no exception @@ -93,8 +101,8 @@ public void connection_request_protobuf_generation_default_standalone_configurat @Test public void connection_request_protobuf_generation_default_cluster_configuration() { // setup - RedisClusterClientConfiguration redisClusterClientConfiguration = - RedisClusterClientConfiguration.builder().build(); + GlideClusterClientConfiguration glideClusterClientConfiguration = + GlideClusterClientConfiguration.builder().build(); ConnectionRequest expectedProtobufConnectionRequest = ConnectionRequest.newBuilder() .setTlsMode(TlsMode.NoTls) @@ -108,7 +116,7 @@ public void connection_request_protobuf_generation_default_cluster_configuration // execute when(channel.connect(eq(expectedProtobufConnectionRequest))).thenReturn(completedFuture); CompletableFuture result = - connectionManager.connectToRedis(redisClusterClientConfiguration); + connectionManager.connectToValkey(glideClusterClientConfiguration); // verify assertNull(result.get()); @@ -119,13 +127,13 @@ public void connection_request_protobuf_generation_default_cluster_configuration @Test public void connection_request_protobuf_generation_with_all_fields_set() { // setup - RedisClientConfiguration redisClientConfiguration = - RedisClientConfiguration.builder() + GlideClientConfiguration glideClientConfiguration = + GlideClientConfiguration.builder() .address(NodeAddress.builder().host(HOST).port(PORT).build()) .address(NodeAddress.builder().host(DEFAULT_HOST).port(DEFAULT_PORT).build()) .useTLS(true) .readFrom(ReadFrom.PREFER_REPLICA) - .credentials(RedisCredentials.builder().username(USERNAME).password(PASSWORD).build()) + .credentials(ServerCredentials.builder().username(USERNAME).password(PASSWORD).build()) .requestTimeout(REQUEST_TIMEOUT) .reconnectStrategy( BackoffStrategy.builder() @@ -135,6 +143,12 @@ public void connection_request_protobuf_generation_with_all_fields_set() { .build()) .databaseId(DATABASE_ID) .clientName(CLIENT_NAME) + .subscriptionConfiguration( + StandaloneSubscriptionConfiguration.builder() + .subscription(EXACT, gs("channel_1")) + .subscription(EXACT, gs("channel_2")) + .subscription(PATTERN, gs("*chatRoom*")) + .build()) .build(); ConnectionRequest expectedProtobufConnectionRequest = ConnectionRequest.newBuilder() @@ -162,6 +176,23 @@ public void connection_request_protobuf_generation_with_all_fields_set() { .build()) .setDatabaseId(DATABASE_ID) .setClientName(CLIENT_NAME) + .setPubsubSubscriptions( + PubSubSubscriptions.newBuilder() + .putAllChannelsOrPatternsByType( + Map.of( + EXACT.ordinal(), + PubSubChannelsOrPatterns.newBuilder() + .addChannelsOrPatterns( + ByteString.copyFrom(gs("channel_1").getBytes())) + .addChannelsOrPatterns( + ByteString.copyFrom(gs("channel_2").getBytes())) + .build(), + PATTERN.ordinal(), + PubSubChannelsOrPatterns.newBuilder() + .addChannelsOrPatterns( + ByteString.copyFrom(gs("*chatRoom*").getBytes())) + .build())) + .build()) .build(); CompletableFuture completedFuture = new CompletableFuture<>(); Response response = Response.newBuilder().setConstantResponse(ConstantResponse.OK).build(); @@ -169,7 +200,7 @@ public void connection_request_protobuf_generation_with_all_fields_set() { // execute when(channel.connect(eq(expectedProtobufConnectionRequest))).thenReturn(completedFuture); - CompletableFuture result = connectionManager.connectToRedis(redisClientConfiguration); + CompletableFuture result = connectionManager.connectToValkey(glideClientConfiguration); // verify assertNull(result.get()); @@ -180,14 +211,14 @@ public void connection_request_protobuf_generation_with_all_fields_set() { @Test public void response_validation_on_constant_response_returns_successfully() { // setup - RedisClientConfiguration redisClientConfiguration = RedisClientConfiguration.builder().build(); + GlideClientConfiguration glideClientConfiguration = GlideClientConfiguration.builder().build(); CompletableFuture completedFuture = new CompletableFuture<>(); Response response = Response.newBuilder().setConstantResponse(ConstantResponse.OK).build(); completedFuture.complete(response); // execute when(channel.connect(any())).thenReturn(completedFuture); - CompletableFuture result = connectionManager.connectToRedis(redisClientConfiguration); + CompletableFuture result = connectionManager.connectToValkey(glideClientConfiguration); // verify assertNull(result.get()); @@ -197,7 +228,7 @@ public void response_validation_on_constant_response_returns_successfully() { @Test public void connection_on_empty_response_throws_ClosingException() { // setup - RedisClientConfiguration redisClientConfiguration = RedisClientConfiguration.builder().build(); + GlideClientConfiguration glideClientConfiguration = GlideClientConfiguration.builder().build(); CompletableFuture completedFuture = new CompletableFuture<>(); Response response = Response.newBuilder().build(); completedFuture.complete(response); @@ -207,7 +238,7 @@ public void connection_on_empty_response_throws_ClosingException() { ExecutionException executionException = assertThrows( ExecutionException.class, - () -> connectionManager.connectToRedis(redisClientConfiguration).get()); + () -> connectionManager.connectToValkey(glideClientConfiguration).get()); assertTrue(executionException.getCause() instanceof ClosingException); assertEquals("Unexpected empty data in response", executionException.getCause().getMessage()); @@ -217,7 +248,7 @@ public void connection_on_empty_response_throws_ClosingException() { @Test public void connection_on_resp_pointer_throws_ClosingException() { // setup - RedisClientConfiguration redisClientConfiguration = RedisClientConfiguration.builder().build(); + GlideClientConfiguration glideClientConfiguration = GlideClientConfiguration.builder().build(); CompletableFuture completedFuture = new CompletableFuture<>(); Response response = Response.newBuilder().setRespPointer(42).build(); completedFuture.complete(response); @@ -227,7 +258,7 @@ public void connection_on_resp_pointer_throws_ClosingException() { ExecutionException executionException = assertThrows( ExecutionException.class, - () -> connectionManager.connectToRedis(redisClientConfiguration).get()); + () -> connectionManager.connectToValkey(glideClientConfiguration).get()); assertTrue(executionException.getCause() instanceof ClosingException); assertEquals("Unexpected data in response", executionException.getCause().getMessage()); diff --git a/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java b/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java index ecf59e4a17..21c8152e72 100644 --- a/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java +++ b/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.utils; import glide.connectors.handlers.ChannelHandler; diff --git a/java/client/src/test/java/glide/utils/RustCoreMock.java b/java/client/src/test/java/glide/utils/RustCoreMock.java index b9bc53bae6..c4daa80803 100644 --- a/java/client/src/test/java/glide/utils/RustCoreMock.java +++ b/java/client/src/test/java/glide/utils/RustCoreMock.java @@ -1,6 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.utils; +import command_request.CommandRequestOuterClass.CommandRequest; import connection_request.ConnectionRequestOuterClass.ConnectionRequest; import glide.connectors.resources.Platform; import io.netty.bootstrap.ServerBootstrap; @@ -24,7 +25,6 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import redis_request.RedisRequestOuterClass.RedisRequest; import response.ResponseOuterClass.ConstantResponse; import response.ResponseOuterClass.Response; @@ -54,10 +54,10 @@ public byte[] handle(byte[] request) { public abstract Response connection(ConnectionRequest request); /** Return `null` to do not reply. */ - public abstract Response.Builder redisRequest(RedisRequest request); + public abstract Response.Builder commandRequest(CommandRequest request); - public Response redisRequestWithCallbackId(RedisRequest request) { - var responseDraft = redisRequest(request); + public Response commandRequestWithCallbackId(CommandRequest request) { + var responseDraft = commandRequest(request); return responseDraft == null ? null : responseDraft.setCallbackIdx(request.getCallbackIdx()).build(); @@ -166,8 +166,8 @@ public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) response = handler.connection(connection); anybodyConnected.setPlain(true); } else { - var request = RedisRequest.parseFrom(bytes); - response = handler.redisRequestWithCallbackId(request); + var request = CommandRequest.parseFrom(bytes); + response = handler.commandRequestWithCallbackId(request); } if (response != null) { ctx.writeAndFlush(response); diff --git a/java/gradle.properties b/java/gradle.properties deleted file mode 100644 index 3013c32a31..0000000000 --- a/java/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -version=1.13.0 -org.gradle.jvmargs=-Duser.language=en -Duser.country=US diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/java/gradle/wrapper/gradle-wrapper.properties index ac72c34e8a..1af9e0930b 100644 --- a/java/gradle/wrapper/gradle-wrapper.properties +++ b/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index f17d5d8ac8..d6c7593820 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -30,9 +30,9 @@ dependencies { testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' } -def standaloneRedisPorts = [] -def clusterRedisPorts = [] -def redisVersion = "" +def standalonePorts = [] +def clusterPorts = [] +def serverVersion = "" ext { extractPortsFromClusterManagerOutput = { String output -> @@ -47,10 +47,12 @@ ext { } return res } - extractRedisVersion = { String output -> - // Line in format like + extractServerVersion = { String output -> + // Redis response: // Redis server v=7.2.3 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=7504b1fedf883f2 - return output.split(" ")[2].split("=")[1] + // Valkey response: + // Server v=7.2.5 sha=26388270:0 malloc=jemalloc-5.3.0 bits=64 build=ea40bb1576e402d6 + return output.split("v=")[1].split(" ")[0] } } @@ -68,7 +70,7 @@ tasks.register('stopAllBeforeTests', Exec) { ignoreExitValue true // ignore fail if servers are stopped before } -// delete dirs if stop failed due to https://github.com/aws/glide-for-redis/issues/849 +// delete dirs if stop failed due to https://github.com/valkey-io/valkey-glide/issues/849 tasks.register('clearDirs', Delete) { delete "${project.rootDir}/../utils/clusters" } @@ -81,7 +83,7 @@ tasks.register('startCluster') { commandLine 'python3', 'cluster_manager.py', 'start', '--cluster-mode' standardOutput = os } - clusterRedisPorts = extractPortsFromClusterManagerOutput(os.toString()) + clusterPorts = extractPortsFromClusterManagerOutput(os.toString()) } } } @@ -94,25 +96,25 @@ tasks.register('startStandalone') { commandLine 'python3', 'cluster_manager.py', 'start', '-r', '0' standardOutput = os } - standaloneRedisPorts = extractPortsFromClusterManagerOutput(os.toString()) + standalonePorts = extractPortsFromClusterManagerOutput(os.toString()) } } } -tasks.register('getRedisVersion') { +tasks.register('getServerVersion') { doLast { new ByteArrayOutputStream().withStream { os -> exec { commandLine 'redis-server', '-v' standardOutput = os } - redisVersion = extractRedisVersion(os.toString()) + serverVersion = extractServerVersion(os.toString()) } } } test.dependsOn 'stopAllBeforeTests' -test.dependsOn 'getRedisVersion' +test.dependsOn 'getServerVersion' stopAllBeforeTests.finalizedBy 'clearDirs' clearDirs.finalizedBy 'startStandalone' clearDirs.finalizedBy 'startCluster' @@ -121,12 +123,12 @@ test.dependsOn ':client:buildRustRelease' tasks.withType(Test) { doFirst { - println "Cluster ports = ${clusterRedisPorts}" - println "Standalone ports = ${standaloneRedisPorts}" - println "Redis version = ${redisVersion}" - systemProperty 'test.redis.standalone.ports', standaloneRedisPorts.join(',') - systemProperty 'test.redis.cluster.ports', clusterRedisPorts.join(',') - systemProperty 'test.redis.version', redisVersion + println "Cluster ports = ${clusterPorts}" + println "Standalone ports = ${standalonePorts}" + println "Server version = ${serverVersion}" + systemProperty 'test.server.standalone.ports', standalonePorts.join(',') + systemProperty 'test.server.cluster.ports', clusterPorts.join(',') + systemProperty 'test.server.version', serverVersion } testLogging { @@ -134,7 +136,10 @@ tasks.withType(Test) { events "started", "skipped", "passed", "failed" showStandardStreams true } - jvmArgs "-Djava.library.path=${project.rootDir}/target/release" + + minHeapSize = "2048m" // Initial heap size. Needed for max size tests. + maxHeapSize = "2048m" // Maximum heap size. Needed for max size tests. + afterTest { desc, result -> logger.quiet "${desc.className}.${desc.name}: ${result.resultType} ${(result.getEndTime() - result.getStartTime())/1000}s" } diff --git a/java/integTest/src/test/java/glide/ConnectionTests.java b/java/integTest/src/test/java/glide/ConnectionTests.java index 254ffad838..9bca84c108 100644 --- a/java/integTest/src/test/java/glide/ConnectionTests.java +++ b/java/integTest/src/test/java/glide/ConnectionTests.java @@ -1,9 +1,9 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; -import glide.api.RedisClient; +import glide.api.GlideClient; +import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -15,8 +15,8 @@ public class ConnectionTests { @SneakyThrows public void basic_client() { var regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address( NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) .build()) @@ -28,8 +28,8 @@ public void basic_client() { @SneakyThrows public void cluster_client() { var regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address(NodeAddress.builder().port(TestConfiguration.CLUSTER_PORTS[0]).build()) .build()) .get(); diff --git a/java/integTest/src/test/java/glide/CustomThreadPoolResourceTest.java b/java/integTest/src/test/java/glide/CustomThreadPoolResourceTest.java index b552f141c1..ce1817022a 100644 --- a/java/integTest/src/test/java/glide/CustomThreadPoolResourceTest.java +++ b/java/integTest/src/test/java/glide/CustomThreadPoolResourceTest.java @@ -1,11 +1,11 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; import static org.junit.jupiter.api.Assertions.assertEquals; -import glide.api.RedisClient; +import glide.api.GlideClient; +import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; import glide.connectors.resources.EpollResource; import glide.connectors.resources.KQueuePoolResource; import glide.connectors.resources.Platform; @@ -32,8 +32,8 @@ public void standalone_client_with_custom_threadPoolResource() { } var regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address( NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) .threadPoolResource(customThreadPoolResource) diff --git a/java/integTest/src/test/java/glide/ErrorHandlingTests.java b/java/integTest/src/test/java/glide/ErrorHandlingTests.java index 2776de3565..a3a56a16ac 100644 --- a/java/integTest/src/test/java/glide/ErrorHandlingTests.java +++ b/java/integTest/src/test/java/glide/ErrorHandlingTests.java @@ -1,13 +1,13 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import glide.api.RedisClient; +import glide.api.GlideClient; +import glide.api.models.configuration.GlideClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.RequestException; import java.net.ServerSocket; @@ -26,8 +26,8 @@ public void basic_client_tries_to_connect_to_wrong_address() { assertThrows( ExecutionException.class, () -> - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address(NodeAddress.builder().port(getFreePort()).build()) .build()) .get()); @@ -40,8 +40,8 @@ public void basic_client_tries_to_connect_to_wrong_address() { @SneakyThrows public void basic_client_tries_wrong_command() { try (var regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address( NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) .build()) @@ -60,8 +60,8 @@ public void basic_client_tries_wrong_command() { @SneakyThrows public void basic_client_tries_wrong_command_arguments() { try (var regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address( NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) .build()) diff --git a/java/integTest/src/test/java/glide/LoggerTests.java b/java/integTest/src/test/java/glide/LoggerTests.java new file mode 100644 index 0000000000..ddf9aa7968 --- /dev/null +++ b/java/integTest/src/test/java/glide/LoggerTests.java @@ -0,0 +1,103 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.api.logging.Logger; +import java.io.File; +import java.util.Scanner; +import java.util.UUID; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +public class LoggerTests { + + private final Logger.Level DEFAULT_TEST_LOG_LEVEL = Logger.Level.WARN; + + @Test + public void init_logger() { + Logger.init(DEFAULT_TEST_LOG_LEVEL); + assertEquals(DEFAULT_TEST_LOG_LEVEL, Logger.getLoggerLevel()); + // The logger is already configured, so calling init again shouldn't modify the log level + Logger.init(Logger.Level.ERROR); + assertEquals(DEFAULT_TEST_LOG_LEVEL, Logger.getLoggerLevel()); + } + + @Test + public void set_logger_config() { + Logger.setLoggerConfig(Logger.Level.INFO); + assertEquals(Logger.Level.INFO, Logger.getLoggerLevel()); + // Revert to the default test log level + Logger.setLoggerConfig(DEFAULT_TEST_LOG_LEVEL); + assertEquals(DEFAULT_TEST_LOG_LEVEL, Logger.getLoggerLevel()); + } + + @SneakyThrows + @Test + public void log_to_file() { + String logFileIdentifier = UUID.randomUUID().toString(); + + String infoIdentifier = "Info"; + String infoMessage = "foo"; + String warnIdentifier = "Warn"; + String warnMessage = "woof"; + String errorIdentifier = "Error"; + String errorMessage = "meow"; + String debugIdentifier = "Debug"; + String debugMessage = "chirp"; + String traceIdentifier = "Trace"; + String traceMessage = "squawk"; + + String filename = logFileIdentifier + "log.txt"; + + Logger.setLoggerConfig(Logger.Level.INFO, filename); + Logger.log(Logger.Level.INFO, infoIdentifier, infoMessage); + Logger.log(Logger.Level.WARN, warnIdentifier, warnMessage); + Logger.log(Logger.Level.ERROR, errorIdentifier, errorMessage); + Logger.log(Logger.Level.DEBUG, debugIdentifier, debugMessage); + Logger.log(Logger.Level.TRACE, traceIdentifier, traceMessage); + + // Test logging with lazily constructed messages + Logger.log(Logger.Level.INFO, infoIdentifier, () -> infoMessage); + Logger.log(Logger.Level.WARN, warnIdentifier, () -> warnMessage); + Logger.log(Logger.Level.ERROR, errorIdentifier, () -> errorMessage); + Logger.log(Logger.Level.DEBUG, debugIdentifier, () -> debugMessage); + Logger.log(Logger.Level.TRACE, traceIdentifier, () -> traceMessage); + + File logFolder = new File("glide-logs"); + + // Initialize a new logger to force closing of existing files + String dummyFilename = "dummy.txt"; + Logger.setLoggerConfig(Logger.Level.DEFAULT, dummyFilename); + File[] dummyLogFiles = logFolder.listFiles((dir, name) -> name.startsWith(dummyFilename + ".")); + assertNotNull(dummyLogFiles); + File dummyLogFile = dummyLogFiles[0]; + + File[] logFiles = logFolder.listFiles((dir, name) -> name.startsWith(filename + ".")); + assertNotNull(logFiles); + File logFile = logFiles[0]; + try (Scanner reader = new Scanner(logFile)) { + String infoLine = reader.nextLine(); + String warnLine = reader.nextLine(); + String errorLine = reader.nextLine(); + String infoLineLazy = reader.nextLine(); + String warnLineLazy = reader.nextLine(); + String errorLineLazy = reader.nextLine(); + assertFalse(reader.hasNextLine()); + + assertTrue(infoLine.contains(infoIdentifier + " - " + infoMessage)); + assertTrue(warnLine.contains(warnIdentifier + " - " + warnMessage)); + assertTrue(errorLine.contains(errorIdentifier + " - " + errorMessage)); + assertTrue(infoLineLazy.contains(infoIdentifier + " - " + infoMessage)); + assertTrue(warnLineLazy.contains(warnIdentifier + " - " + warnMessage)); + assertTrue(errorLineLazy.contains(errorIdentifier + " - " + errorMessage)); + } finally { + logFile.delete(); + dummyLogFile.delete(); + logFolder.delete(); + } + } +} diff --git a/java/integTest/src/test/java/glide/PubSubTests.java b/java/integTest/src/test/java/glide/PubSubTests.java new file mode 100644 index 0000000000..6ca9b3691f --- /dev/null +++ b/java/integTest/src/test/java/glide/PubSubTests.java @@ -0,0 +1,1244 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide; + +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.commonClusterClientConfig; +import static glide.api.BaseClient.OK; +import static glide.api.models.GlideString.gs; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import glide.api.BaseClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; +import glide.api.models.ClusterTransaction; +import glide.api.models.GlideString; +import glide.api.models.PubSubMessage; +import glide.api.models.Transaction; +import glide.api.models.configuration.BaseSubscriptionConfiguration.ChannelMode; +import glide.api.models.configuration.BaseSubscriptionConfiguration.MessageCallback; +import glide.api.models.configuration.ClusterSubscriptionConfiguration; +import glide.api.models.configuration.ClusterSubscriptionConfiguration.PubSubClusterChannelMode; +import glide.api.models.configuration.StandaloneSubscriptionConfiguration; +import glide.api.models.configuration.StandaloneSubscriptionConfiguration.PubSubChannelMode; +import glide.api.models.exceptions.ConfigurationError; +import glide.api.models.exceptions.RequestException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +@Timeout(30) // sec +public class PubSubTests { + + // TODO protocol version + @SneakyThrows + @SuppressWarnings("unchecked") + private BaseClient createClientWithSubscriptions( + boolean standalone, + Map> subscriptions, + Optional callback, + Optional context) { + if (standalone) { + var subConfigBuilder = + StandaloneSubscriptionConfiguration.builder() + .subscriptions((Map>) subscriptions); + + if (callback.isPresent()) { + subConfigBuilder.callback(callback.get(), context.get()); + } + return GlideClient.createClient( + commonClientConfig() + .requestTimeout(5000) + .subscriptionConfiguration(subConfigBuilder.build()) + .build()) + .get(); + } else { + var subConfigBuilder = + ClusterSubscriptionConfiguration.builder() + .subscriptions((Map>) subscriptions); + + if (callback.isPresent()) { + subConfigBuilder.callback(callback.get(), context.get()); + } + + return GlideClusterClient.createClient( + commonClusterClientConfig() + .requestTimeout(5000) + .subscriptionConfiguration(subConfigBuilder.build()) + .build()) + .get(); + } + } + + private BaseClient createClientWithSubscriptions( + boolean standalone, Map> subscriptions) { + return createClientWithSubscriptions( + standalone, subscriptions, Optional.empty(), Optional.empty()); + } + + @SneakyThrows + private BaseClient createClient(boolean standalone) { + if (standalone) { + return GlideClient.createClient(commonClientConfig().build()).get(); + } + return GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); + } + + /** + * pubsubMessage queue used in callback to analyze received pubsubMessages. Number is a client ID. + */ + private final ConcurrentLinkedDeque> pubsubMessageQueue = + new ConcurrentLinkedDeque<>(); + + /** Clients used in a test. */ + private final List clients = new ArrayList<>(); + + private static final int MESSAGE_DELIVERY_DELAY = 500; // ms + + @BeforeEach + @SneakyThrows + public void cleanup() { + for (var client : clients) { + if (client instanceof GlideClusterClient) { + ((GlideClusterClient) client).customCommand(new String[] {"unsubscribe"}, ALL_NODES).get(); + ((GlideClusterClient) client).customCommand(new String[] {"punsubscribe"}, ALL_NODES).get(); + ((GlideClusterClient) client).customCommand(new String[] {"sunsubscribe"}, ALL_NODES).get(); + } else { + ((GlideClient) client).customCommand(new String[] {"unsubscribe"}).get(); + ((GlideClient) client).customCommand(new String[] {"punsubscribe"}).get(); + } + client.close(); + } + clients.clear(); + pubsubMessageQueue.clear(); + } + + private enum MessageReadMethod { + Callback, + Async, + Sync + } + + @SneakyThrows + private void verifyReceivedPubsubMessages( + Set> pubsubMessages, + BaseClient listener, + MessageReadMethod method) { + if (method == MessageReadMethod.Callback) { + assertEquals(pubsubMessages, new HashSet<>(pubsubMessageQueue)); + } else if (method == MessageReadMethod.Async) { + var received = new HashSet(pubsubMessages.size()); + CompletableFuture messagePromise; + while ((messagePromise = listener.getPubSubMessage()).isDone()) { + received.add(messagePromise.get()); + } + assertEquals( + pubsubMessages.stream().map(Pair::getValue).collect(Collectors.toSet()), received); + } else { // Sync + var received = new HashSet(pubsubMessages.size()); + PubSubMessage message; + while ((message = listener.tryGetPubSubMessage()) != null) { + received.add(message); + } + assertEquals( + pubsubMessages.stream().map(Pair::getValue).collect(Collectors.toSet()), received); + } + } + + /** Permute all combinations of `standalone` as bool vs {@link MessageReadMethod}. */ + private static Stream getTestScenarios() { + return Stream.of( + Arguments.of(true, MessageReadMethod.Callback), + Arguments.of(true, MessageReadMethod.Sync), + Arguments.of(true, MessageReadMethod.Async), + Arguments.of(false, MessageReadMethod.Callback), + Arguments.of(false, MessageReadMethod.Sync), + Arguments.of(false, MessageReadMethod.Async)); + } + + private ChannelMode exact(boolean standalone) { + return standalone ? PubSubChannelMode.EXACT : PubSubClusterChannelMode.EXACT; + } + + private ChannelMode pattern(boolean standalone) { + return standalone ? PubSubChannelMode.PATTERN : PubSubClusterChannelMode.PATTERN; + } + + @SuppressWarnings("unchecked") + private BaseClient createListener( + boolean standalone, + boolean withCallback, + int clientId, + Map> subscriptions) { + MessageCallback callback = + (msg, ctx) -> + ((ConcurrentLinkedDeque>) ctx) + .push(Pair.of(clientId, msg)); + return withCallback + ? createClientWithSubscriptions( + standalone, subscriptions, Optional.of(callback), Optional.of(pubsubMessageQueue)) + : createClientWithSubscriptions(standalone, subscriptions); + } + + // TODO why `publish` returns 0 on cluster or > 1 on standalone when there is only 1 receiver??? + // meanwhile, all pubsubMessages are delivered. + // debug this and add checks for `publish` return value + + // TODO: remove once fixed + private void skipTestsOnMac() { + assumeFalse( + System.getProperty("os.name").toLowerCase().contains("mac"), + "PubSub doesn't work on mac OS"); + } + + /** Similar to `test_pubsub_exact_happy_path` in python client tests. */ + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void exact_happy_path(boolean standalone, MessageReadMethod method) { + skipTestsOnMac(); + GlideString channel = gs(UUID.randomUUID().toString()); + GlideString message = gs(UUID.randomUUID().toString()); + var subscriptions = Map.of(exact(standalone), Set.of(channel)); + + var listener = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = createClient(standalone); + clients.addAll(List.of(listener, sender)); + + sender.publish(message, channel).get(); + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the message + + verifyReceivedPubsubMessages( + Set.of(Pair.of(1, new PubSubMessage(message, channel))), listener, method); + } + + /** Similar to `test_pubsub_exact_happy_path_many_channels` in python client tests. */ + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void exact_happy_path_many_channels(boolean standalone, MessageReadMethod method) { + skipTestsOnMac(); + int numChannels = 256; + int messagesPerChannel = 256; + var messages = new ArrayList(numChannels * messagesPerChannel); + ChannelMode mode = exact(standalone); + Map> subscriptions = Map.of(mode, new HashSet<>()); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(i + "-" + UUID.randomUUID()); + subscriptions.get(mode).add(channel); + for (var j = 0; j < messagesPerChannel; j++) { + GlideString message = gs(i + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel)); + } + } + + var listener = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = createClient(standalone); + clients.addAll(List.of(listener, sender)); + + for (var pubsubMessage : messages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + verifyReceivedPubsubMessages( + messages.stream().map(m -> Pair.of(1, m)).collect(Collectors.toSet()), listener, method); + } + + /** Similar to `test_sharded_pubsub` in python client tests. */ + @SneakyThrows + @ParameterizedTest + @EnumSource(MessageReadMethod.class) + public void sharded_pubsub(MessageReadMethod method) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + + GlideString channel = gs(UUID.randomUUID().toString()); + GlideString pubsubMessage = gs(UUID.randomUUID().toString()); + var subscriptions = Map.of(PubSubClusterChannelMode.SHARDED, Set.of(channel)); + + var listener = createListener(false, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = (GlideClusterClient) createClient(false); + clients.addAll(List.of(listener, sender)); + + sender.publish(pubsubMessage, channel, true).get(); + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the message + + verifyReceivedPubsubMessages( + Set.of(Pair.of(1, new PubSubMessage(pubsubMessage, channel))), listener, method); + } + + /** Similar to `test_sharded_pubsub_many_channels` in python client tests. */ + @SneakyThrows + @ParameterizedTest + @EnumSource(MessageReadMethod.class) + public void sharded_pubsub_many_channels(MessageReadMethod method) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + + int numChannels = 256; + int pubsubMessagesPerChannel = 256; + var pubsubMessages = new ArrayList(numChannels * pubsubMessagesPerChannel); + PubSubClusterChannelMode mode = PubSubClusterChannelMode.SHARDED; + Map> subscriptions = Map.of(mode, new HashSet<>()); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(i + "-" + UUID.randomUUID()); + subscriptions.get(mode).add(channel); + for (var j = 0; j < pubsubMessagesPerChannel; j++) { + GlideString message = gs(i + "-" + j + "-" + UUID.randomUUID()); + pubsubMessages.add(new PubSubMessage(message, channel)); + } + } + + var listener = createListener(false, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = (GlideClusterClient) createClient(false); + clients.addAll(List.of(listener, sender)); + + for (var pubsubMessage : pubsubMessages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel(), true).get(); + } + sender.publish(UUID.randomUUID().toString(), UUID.randomUUID().toString(), true).get(); + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + verifyReceivedPubsubMessages( + pubsubMessages.stream().map(m -> Pair.of(1, m)).collect(Collectors.toSet()), + listener, + method); + } + + /** Similar to `test_pubsub_pattern` in python client tests. */ + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void pattern(boolean standalone, MessageReadMethod method) { + skipTestsOnMac(); + String prefix = "channel."; + GlideString pattern = gs(prefix + "*"); + Map message2channels = + Map.of( + gs(prefix + "1"), + gs(UUID.randomUUID().toString()), + gs(prefix + "2"), + gs(UUID.randomUUID().toString())); + var subscriptions = + Map.of( + standalone ? PubSubChannelMode.PATTERN : PubSubClusterChannelMode.PATTERN, + Set.of(pattern)); + + var listener = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = createClient(standalone); + clients.addAll(List.of(listener, sender)); + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // need some time to propagate subscriptions - why? + + for (var entry : message2channels.entrySet()) { + sender.publish(entry.getValue(), entry.getKey()).get(); + } + sender.publish(UUID.randomUUID().toString(), "channel").get(); + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + var expected = + message2channels.entrySet().stream() + .map(e -> Pair.of(1, new PubSubMessage(e.getValue(), e.getKey(), pattern))) + .collect(Collectors.toSet()); + + verifyReceivedPubsubMessages(expected, listener, method); + } + + /** Similar to `test_pubsub_pattern_many_channels` in python client tests. */ + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void pattern_many_channels(boolean standalone, MessageReadMethod method) { + skipTestsOnMac(); + String prefix = "channel."; + GlideString pattern = gs(prefix + "*"); + int numChannels = 256; + int messagesPerChannel = 256; + ChannelMode mode = standalone ? PubSubChannelMode.PATTERN : PubSubClusterChannelMode.PATTERN; + var messages = new ArrayList(numChannels * messagesPerChannel); + var subscriptions = Map.of(mode, Set.of(pattern)); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(prefix + "-" + i + "-" + UUID.randomUUID()); + for (var j = 0; j < messagesPerChannel; j++) { + GlideString message = gs(i + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel, pattern)); + } + } + + var listener = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = createClient(standalone); + clients.addAll(List.of(listener, sender)); + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // need some time to propagate subscriptions - why? + + for (var pubsubMessage : messages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + sender.publish(UUID.randomUUID().toString(), "channel").get(); + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + verifyReceivedPubsubMessages( + messages.stream().map(m -> Pair.of(1, m)).collect(Collectors.toSet()), listener, method); + } + + /** Similar to `test_pubsub_combined_exact_and_pattern_one_client` in python client tests. */ + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void combined_exact_and_pattern_one_client(boolean standalone, MessageReadMethod method) { + skipTestsOnMac(); + String prefix = "channel."; + GlideString pattern = gs(prefix + "*"); + int numChannels = 256; + int messagesPerChannel = 256; + var messages = new ArrayList(numChannels * messagesPerChannel); + ChannelMode mode = standalone ? PubSubChannelMode.EXACT : PubSubClusterChannelMode.EXACT; + Map> subscriptions = + Map.of( + mode, + new HashSet<>(), + standalone ? PubSubChannelMode.PATTERN : PubSubClusterChannelMode.PATTERN, + Set.of(pattern)); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(i + "-" + UUID.randomUUID()); + subscriptions.get(mode).add(channel); + for (var j = 0; j < messagesPerChannel; j++) { + GlideString message = gs(i + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel)); + } + } + + for (var j = 0; j < messagesPerChannel; j++) { + GlideString pubsubMessage = gs(j + "-" + UUID.randomUUID()); + GlideString channel = gs(prefix + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(pubsubMessage, channel, pattern)); + } + + var listener = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = createClient(standalone); + clients.addAll(List.of(listener, sender)); + + for (var pubsubMessage : messages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + verifyReceivedPubsubMessages( + messages.stream().map(m -> Pair.of(1, m)).collect(Collectors.toSet()), listener, method); + } + + /** + * Similar to `test_pubsub_combined_exact_and_pattern_multiple_clients` in python client tests. + */ + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void combined_exact_and_pattern_multiple_clients( + boolean standalone, MessageReadMethod method) { + skipTestsOnMac(); + String prefix = "channel."; + GlideString pattern = gs(prefix + "*"); + int numChannels = 256; + var messages = new ArrayList(numChannels * 2); + ChannelMode mode = exact(standalone); + Map> subscriptions = Map.of(mode, new HashSet<>()); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(i + "-" + UUID.randomUUID()); + subscriptions.get(mode).add(channel); + GlideString message = gs(i + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel)); + } + + for (var j = 0; j < numChannels; j++) { + GlideString message = gs(j + "-" + UUID.randomUUID()); + GlideString channel = gs(prefix + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel, pattern)); + } + + var listenerExactSub = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + + subscriptions = Map.of(pattern(standalone), Set.of(pattern)); + var listenerPatternSub = + createListener(standalone, method == MessageReadMethod.Callback, 2, subscriptions); + + var sender = createClient(standalone); + clients.addAll(List.of(listenerExactSub, listenerPatternSub, sender)); + + for (var pubsubMessage : messages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + if (method == MessageReadMethod.Callback) { + verifyReceivedPubsubMessages( + messages.stream() + .map(m -> Pair.of(m.getPattern().isEmpty() ? 1 : 2, m)) + .collect(Collectors.toSet()), + listenerExactSub, + method); + } else { + verifyReceivedPubsubMessages( + messages.stream() + .filter(m -> m.getPattern().isEmpty()) + .map(m -> Pair.of(1, m)) + .collect(Collectors.toSet()), + listenerExactSub, + method); + verifyReceivedPubsubMessages( + messages.stream() + .filter(m -> m.getPattern().isPresent()) + .map(m -> Pair.of(2, m)) + .collect(Collectors.toSet()), + listenerPatternSub, + method); + } + } + + /** + * Similar to `test_pubsub_combined_exact_pattern_and_sharded_one_client` in python client tests. + */ + @SneakyThrows + @ParameterizedTest + @EnumSource(MessageReadMethod.class) + public void combined_exact_pattern_and_sharded_one_client(MessageReadMethod method) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + + String prefix = "channel."; + GlideString pattern = gs(prefix + "*"); + String shardPrefix = "{shard}"; + int numChannels = 256; + var messages = new ArrayList(numChannels * 2); + var shardedMessages = new ArrayList(numChannels); + Map> subscriptions = + Map.of( + PubSubClusterChannelMode.EXACT, new HashSet<>(), + PubSubClusterChannelMode.PATTERN, Set.of(pattern), + PubSubClusterChannelMode.SHARDED, new HashSet<>()); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(i + "-" + UUID.randomUUID()); + subscriptions.get(PubSubClusterChannelMode.EXACT).add(channel); + GlideString message = gs(i + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel)); + } + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(shardPrefix + "-" + i + "-" + UUID.randomUUID()); + subscriptions.get(PubSubClusterChannelMode.SHARDED).add(channel); + GlideString message = gs(i + "-" + UUID.randomUUID()); + shardedMessages.add(new PubSubMessage(message, channel)); + } + + for (var j = 0; j < numChannels; j++) { + GlideString message = gs(j + "-" + UUID.randomUUID()); + GlideString channel = gs(prefix + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel, pattern)); + } + + var listener = createListener(false, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = (GlideClusterClient) createClient(false); + clients.addAll(List.of(listener, sender)); + + for (var pubsubMessage : messages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + for (var pubsubMessage : shardedMessages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel(), true).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + messages.addAll(shardedMessages); + verifyReceivedPubsubMessages( + messages.stream().map(m -> Pair.of(1, m)).collect(Collectors.toSet()), listener, method); + } + + /** This test fully covers all `test_pubsub_*_co_existence` tests in python client. */ + @SneakyThrows + @Test + public void coexistense_of_sync_and_async_read() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + + String prefix = "channel."; + String pattern = prefix + "*"; + String shardPrefix = "{shard}"; + int numChannels = 256; + var messages = new ArrayList(numChannels * 2); + var shardedMessages = new ArrayList(numChannels); + Map> subscriptions = + Map.of( + PubSubClusterChannelMode.EXACT, new HashSet<>(), + PubSubClusterChannelMode.PATTERN, Set.of(gs(pattern)), + PubSubClusterChannelMode.SHARDED, new HashSet<>()); + + for (var i = 0; i < numChannels; i++) { + var channel = gs(i + "-" + UUID.randomUUID()); + subscriptions.get(PubSubClusterChannelMode.EXACT).add(channel); + var message = gs(i + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel)); + } + + for (var i = 0; i < numChannels; i++) { + var channel = gs(shardPrefix + "-" + i + "-" + UUID.randomUUID()); + subscriptions.get(PubSubClusterChannelMode.SHARDED).add(channel); + var message = gs(i + "-" + UUID.randomUUID()); + shardedMessages.add(new PubSubMessage(message, channel)); + } + + for (var j = 0; j < numChannels; j++) { + var message = gs(j + "-" + UUID.randomUUID()); + var channel = gs(prefix + "-" + j + "-" + UUID.randomUUID()); + messages.add(new PubSubMessage(message, channel, gs(pattern))); + } + + var listener = createListener(false, false, 1, subscriptions); + var sender = (GlideClusterClient) createClient(false); + clients.addAll(List.of(listener, sender)); + + for (var pubsubMessage : messages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + for (var pubsubMessage : shardedMessages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel(), true).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + messages.addAll(shardedMessages); + + var received = new LinkedHashSet(messages.size()); + var rand = new Random(); + while (true) { + if (rand.nextBoolean()) { + CompletableFuture messagePromise = listener.getPubSubMessage(); + if (messagePromise.isDone()) { + received.add(messagePromise.get()); + } else { + break; // all messages read + } + } else { + var message = listener.tryGetPubSubMessage(); + if (message != null) { + received.add(message); + } else { + break; // all messages read + } + } + } + + // valkey can reorder the messages, so we can't validate that the order (without big delays + // between sends) + assertEquals(new LinkedHashSet<>(messages), received); + } + + /** + * Similar to `test_pubsub_combined_exact_pattern_and_sharded_multi_client` in python client + * tests. + */ + @SneakyThrows + @ParameterizedTest + @EnumSource(MessageReadMethod.class) + public void combined_exact_pattern_and_sharded_multi_client(MessageReadMethod method) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + + String prefix = "channel."; + GlideString pattern = gs(prefix + "*"); + String shardPrefix = "{shard}"; + int numChannels = 256; + var exactMessages = new ArrayList(numChannels); + var patternMessages = new ArrayList(numChannels); + var shardedMessages = new ArrayList(numChannels); + Map> subscriptionsExact = + Map.of(PubSubClusterChannelMode.EXACT, new HashSet<>()); + Map> subscriptionsPattern = + Map.of(PubSubClusterChannelMode.PATTERN, Set.of(pattern)); + Map> subscriptionsSharded = + Map.of(PubSubClusterChannelMode.SHARDED, new HashSet<>()); + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(i + "-" + UUID.randomUUID()); + subscriptionsExact.get(PubSubClusterChannelMode.EXACT).add(channel); + GlideString pubsubMessage = gs(i + "-" + UUID.randomUUID()); + exactMessages.add(new PubSubMessage(pubsubMessage, channel)); + } + + for (var i = 0; i < numChannels; i++) { + GlideString channel = gs(shardPrefix + "-" + i + "-" + UUID.randomUUID()); + subscriptionsSharded.get(PubSubClusterChannelMode.SHARDED).add(channel); + GlideString message = gs(i + "-" + UUID.randomUUID()); + shardedMessages.add(new PubSubMessage(message, channel)); + } + + for (var j = 0; j < numChannels; j++) { + GlideString message = gs(j + "-" + UUID.randomUUID()); + GlideString channel = gs(prefix + "-" + j + "-" + UUID.randomUUID()); + patternMessages.add(new PubSubMessage(message, channel, pattern)); + } + + var listenerExact = + createListener( + false, + method == MessageReadMethod.Callback, + PubSubClusterChannelMode.EXACT.ordinal(), + subscriptionsExact); + var listenerPattern = + createListener( + false, + method == MessageReadMethod.Callback, + PubSubClusterChannelMode.PATTERN.ordinal(), + subscriptionsPattern); + var listenerSharded = + createListener( + false, + method == MessageReadMethod.Callback, + PubSubClusterChannelMode.SHARDED.ordinal(), + subscriptionsSharded); + + var sender = (GlideClusterClient) createClient(false); + clients.addAll(List.of(listenerExact, listenerPattern, listenerSharded, sender)); + + for (var pubsubMessage : exactMessages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + for (var pubsubMessage : patternMessages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel()).get(); + } + for (var pubsubMessage : shardedMessages) { + sender.publish(pubsubMessage.getMessage(), pubsubMessage.getChannel(), true).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + if (method == MessageReadMethod.Callback) { + var expected = new HashSet>(); + expected.addAll( + exactMessages.stream() + .map(m -> Pair.of(PubSubClusterChannelMode.EXACT.ordinal(), m)) + .collect(Collectors.toSet())); + expected.addAll( + patternMessages.stream() + .map(m -> Pair.of(PubSubClusterChannelMode.PATTERN.ordinal(), m)) + .collect(Collectors.toSet())); + expected.addAll( + shardedMessages.stream() + .map(m -> Pair.of(PubSubClusterChannelMode.SHARDED.ordinal(), m)) + .collect(Collectors.toSet())); + + verifyReceivedPubsubMessages(expected, listenerExact, method); + } else { + verifyReceivedPubsubMessages( + exactMessages.stream() + .map(m -> Pair.of(PubSubClusterChannelMode.EXACT.ordinal(), m)) + .collect(Collectors.toSet()), + listenerExact, + method); + verifyReceivedPubsubMessages( + patternMessages.stream() + .map(m -> Pair.of(PubSubClusterChannelMode.PATTERN.ordinal(), m)) + .collect(Collectors.toSet()), + listenerPattern, + method); + verifyReceivedPubsubMessages( + shardedMessages.stream() + .map(m -> Pair.of(PubSubClusterChannelMode.SHARDED.ordinal(), m)) + .collect(Collectors.toSet()), + listenerSharded, + method); + } + } + + /** + * Similar to `test_pubsub_three_publishing_clients_same_name_with_sharded` in python client + * tests. + */ + @SneakyThrows + @ParameterizedTest + @EnumSource(MessageReadMethod.class) + public void three_publishing_clients_same_name_with_sharded(MessageReadMethod method) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + + GlideString channel = gs(UUID.randomUUID().toString()); + var exactMessage = new PubSubMessage(gs(UUID.randomUUID().toString()), channel); + var patternMessage = new PubSubMessage(gs(UUID.randomUUID().toString()), channel, channel); + var shardedMessage = new PubSubMessage(gs(UUID.randomUUID().toString()), channel); + Map> subscriptionsExact = + Map.of(PubSubClusterChannelMode.EXACT, Set.of(channel)); + Map> subscriptionsPattern = + Map.of(PubSubClusterChannelMode.PATTERN, Set.of(channel)); + Map> subscriptionsSharded = + Map.of(PubSubClusterChannelMode.SHARDED, Set.of(channel)); + + var listenerExact = + method == MessageReadMethod.Callback + ? (GlideClusterClient) + createListener( + false, true, PubSubClusterChannelMode.EXACT.ordinal(), subscriptionsExact) + : (GlideClusterClient) createClientWithSubscriptions(false, subscriptionsExact); + + var listenerPattern = + method == MessageReadMethod.Callback + ? createListener( + false, true, PubSubClusterChannelMode.PATTERN.ordinal(), subscriptionsPattern) + : (GlideClusterClient) createClientWithSubscriptions(false, subscriptionsPattern); + + var listenerSharded = + method == MessageReadMethod.Callback + ? createListener( + false, true, PubSubClusterChannelMode.SHARDED.ordinal(), subscriptionsSharded) + : (GlideClusterClient) createClientWithSubscriptions(false, subscriptionsSharded); + + clients.addAll(List.of(listenerExact, listenerPattern, listenerSharded)); + + listenerPattern.publish(exactMessage.getMessage(), channel).get(); + listenerSharded.publish(patternMessage.getMessage(), channel).get(); + listenerExact.publish(shardedMessage.getMessage(), channel, true).get(); + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + if (method == MessageReadMethod.Callback) { + var expected = + Set.of( + Pair.of(PubSubClusterChannelMode.EXACT.ordinal(), exactMessage), + Pair.of( + PubSubClusterChannelMode.EXACT.ordinal(), + new PubSubMessage(patternMessage.getMessage(), channel)), + Pair.of(PubSubClusterChannelMode.PATTERN.ordinal(), patternMessage), + Pair.of( + PubSubClusterChannelMode.PATTERN.ordinal(), + new PubSubMessage(exactMessage.getMessage(), channel, channel)), + Pair.of(PubSubClusterChannelMode.SHARDED.ordinal(), shardedMessage)); + + verifyReceivedPubsubMessages(expected, listenerExact, method); + } else { + verifyReceivedPubsubMessages( + Set.of( + Pair.of(PubSubClusterChannelMode.EXACT.ordinal(), exactMessage), + Pair.of( + PubSubClusterChannelMode.EXACT.ordinal(), + new PubSubMessage(patternMessage.getMessage(), channel))), + listenerExact, + method); + verifyReceivedPubsubMessages( + Set.of( + Pair.of(PubSubClusterChannelMode.PATTERN.ordinal(), patternMessage), + Pair.of( + PubSubClusterChannelMode.PATTERN.ordinal(), + new PubSubMessage(exactMessage.getMessage(), channel, channel))), + listenerPattern, + method); + verifyReceivedPubsubMessages( + Set.of(Pair.of(PubSubClusterChannelMode.SHARDED.ordinal(), shardedMessage)), + listenerSharded, + method); + } + } + + @SneakyThrows + @Test + public void error_cases() { + skipTestsOnMac(); + // client isn't configured with subscriptions + var client = createClient(true); + assertThrows(ConfigurationError.class, client::tryGetPubSubMessage); + client.close(); + + // client configured with callback and doesn't return pubsubMessages via API + MessageCallback callback = (msg, ctx) -> fail(); + client = + createClientWithSubscriptions( + true, Map.of(), Optional.of(callback), Optional.of(pubsubMessageQueue)); + assertThrows(ConfigurationError.class, client::tryGetPubSubMessage); + client.close(); + + // using sharded channels from different slots in a transaction causes a cross slot error + var clusterClient = (GlideClusterClient) createClient(false); + var transaction = + new ClusterTransaction() + .publish("one", "abc", true) + .publish("two", "mnk", true) + .publish("three", "xyz", true); + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.exec(transaction).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("crossslot")); + } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}, read messages via {1}") + @MethodSource("getTestScenarios") + public void transaction_with_all_types_of_messages(boolean standalone, MessageReadMethod method) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + skipTestsOnMac(); + assumeTrue( + standalone, // TODO activate tests after fix + "Test doesn't work on cluster due to Cross Slot error, probably a bug in `redis-rs`"); + + String prefix = "channel"; + GlideString pattern = gs(prefix + "*"); + GlideString shardPrefix = gs("{shard}"); + GlideString channel = gs(UUID.randomUUID().toString()); + var exactMessage = new PubSubMessage(gs(UUID.randomUUID().toString()), channel); + var patternMessage = new PubSubMessage(gs(UUID.randomUUID().toString()), gs(prefix), pattern); + var shardedMessage = new PubSubMessage(gs(UUID.randomUUID().toString()), shardPrefix); + + Map> subscriptions = + standalone + ? Map.of( + PubSubChannelMode.EXACT, + Set.of(channel), + PubSubChannelMode.PATTERN, + Set.of(pattern)) + : Map.of( + PubSubClusterChannelMode.EXACT, + Set.of(channel), + PubSubClusterChannelMode.PATTERN, + Set.of(pattern), + PubSubClusterChannelMode.SHARDED, + Set.of(shardPrefix)); + + var listener = + createListener(standalone, method == MessageReadMethod.Callback, 1, subscriptions); + var sender = createClient(standalone); + clients.addAll(List.of(listener, sender)); + + if (standalone) { + var transaction = + new Transaction() + .publish(exactMessage.getMessage(), exactMessage.getChannel()) + .publish(patternMessage.getMessage(), patternMessage.getChannel()); + ((GlideClient) sender).exec(transaction).get(); + } else { + var transaction = + new ClusterTransaction() + .publish(shardedMessage.getMessage(), shardedMessage.getChannel(), true) + .publish(exactMessage.getMessage(), exactMessage.getChannel()) + .publish(patternMessage.getMessage(), patternMessage.getChannel()); + ((GlideClusterClient) sender).exec(transaction).get(); + } + + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + var expected = + standalone + ? Set.of(Pair.of(1, exactMessage), Pair.of(1, patternMessage)) + : Set.of( + Pair.of(1, exactMessage), Pair.of(1, patternMessage), Pair.of(1, shardedMessage)); + verifyReceivedPubsubMessages(expected, listener, method); + } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + @Disabled( + "No way of currently testing this, see https://github.com/valkey-io/valkey-glide/issues/1649") + public void pubsub_exact_max_size_message(boolean standalone) { + final GlideString channel = gs(UUID.randomUUID().toString()); + final GlideString message = gs("1".repeat(512 * 1024 * 1024)); // 512MB + final GlideString message2 = gs("2".repeat(1 << 25)); // 3MB + + Map> subscriptions = + standalone + ? Map.of(PubSubChannelMode.EXACT, Set.of(channel)) + : Map.of(PubSubClusterChannelMode.EXACT, Set.of(channel)); + var listener = createClientWithSubscriptions(standalone, subscriptions); + var sender = createClient(standalone); + clients.addAll(Arrays.asList(listener, sender)); + + assertEquals(OK, sender.publish(message, channel).get()); + assertEquals(OK, sender.publish(message2, channel).get()); + + // Allow the message to propagate. + Thread.sleep(MESSAGE_DELIVERY_DELAY); + + PubSubMessage asyncMessage = listener.getPubSubMessage().get(); + assertEquals(message, asyncMessage.getMessage()); + assertEquals(channel, asyncMessage.getChannel()); + assertTrue(asyncMessage.getPattern().isEmpty()); + + PubSubMessage syncMessage = listener.tryGetPubSubMessage(); + assertEquals(message2, syncMessage.getMessage()); + assertEquals(channel, syncMessage.getChannel()); + assertTrue(syncMessage.getPattern().isEmpty()); + + // Assert there are no more messages to read. + assertThrows( + TimeoutException.class, () -> listener.getPubSubMessage().get(3, TimeUnit.SECONDS)); + assertNull(listener.tryGetPubSubMessage()); + } + + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {false}) + @Disabled( + "No way of currently testing this, see https://github.com/valkey-io/valkey-glide/issues/1649") + public void pubsub_sharded_max_size_message(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + final GlideString channel = gs(UUID.randomUUID().toString()); + final GlideString message = gs("1".repeat(512 * 1024 * 1024)); // 512MB + final GlideString message2 = gs("2".repeat(1 << 25)); // 3MB + + Map> subscriptions = + Map.of(PubSubClusterChannelMode.SHARDED, Set.of(channel)); + var listener = createClientWithSubscriptions(standalone, subscriptions); + var sender = createClient(standalone); + clients.addAll(Arrays.asList(listener, sender)); + + assertEquals(OK, sender.publish(message, channel).get()); + assertEquals(OK, ((GlideClusterClient) sender).publish(message2, channel, true).get()); + + // Allow the message to propagate. + Thread.sleep(MESSAGE_DELIVERY_DELAY); + + PubSubMessage asyncMessage = listener.getPubSubMessage().get(); + assertEquals(message, asyncMessage.getMessage()); + assertEquals(channel, asyncMessage.getChannel()); + assertTrue(asyncMessage.getPattern().isEmpty()); + + PubSubMessage syncMessage = listener.tryGetPubSubMessage(); + assertEquals(message2, syncMessage.getMessage()); + assertEquals(channel, syncMessage.getChannel()); + assertTrue(syncMessage.getPattern().isEmpty()); + + // Assert there are no more messages to read. + assertThrows( + TimeoutException.class, + () -> { + listener.getPubSubMessage().get(3, TimeUnit.SECONDS); + }); + + assertNull(listener.tryGetPubSubMessage()); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + @Disabled( + "No way of currently testing this, see https://github.com/valkey-io/valkey-glide/issues/1649") + public void pubsub_exact_max_size_message_callback(boolean standalone) { + final GlideString channel = gs(UUID.randomUUID().toString()); + final GlideString message = gs("1".repeat(512 * 1024 * 1024)); // 512MB + + ArrayList callbackMessages = new ArrayList<>(); + final MessageCallback callback = + (pubSubMessage, context) -> { + ArrayList receivedMessages = (ArrayList) context; + receivedMessages.add(pubSubMessage); + }; + + Map> subscriptions = + standalone + ? Map.of(PubSubChannelMode.EXACT, Set.of(channel)) + : Map.of(PubSubClusterChannelMode.EXACT, Set.of(channel)); + + var listener = + createClientWithSubscriptions( + standalone, + subscriptions, + Optional.ofNullable(callback), + Optional.of(callbackMessages)); + var sender = createClient(standalone); + clients.addAll(Arrays.asList(listener, sender)); + + assertEquals(OK, sender.publish(message, channel).get()); + + // Allow the message to propagate. + Thread.sleep(MESSAGE_DELIVERY_DELAY); + + assertEquals(1, callbackMessages.size()); + assertEquals(message, callbackMessages.get(0).getMessage()); + assertEquals(channel, callbackMessages.get(0).getChannel()); + assertTrue(callbackMessages.get(0).getPattern().isEmpty()); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {false}) + @Disabled( + "No way of currently testing this, see https://github.com/valkey-io/valkey-glide/issues/1649") + public void pubsub_sharded_max_size_message_callback(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + final GlideString channel = gs(UUID.randomUUID().toString()); + final GlideString message = gs("1".repeat(512 * 1024 * 1024)); // 512MB + + ArrayList callbackMessages = new ArrayList<>(); + final MessageCallback callback = + (pubSubMessage, context) -> { + ArrayList receivedMessages = (ArrayList) context; + receivedMessages.add(pubSubMessage); + }; + + Map> subscriptions = + Map.of(PubSubClusterChannelMode.SHARDED, Set.of(channel)); + + var listener = + createClientWithSubscriptions( + standalone, + subscriptions, + Optional.ofNullable(callback), + Optional.of(callbackMessages)); + var sender = createClient(standalone); + clients.addAll(Arrays.asList(listener, sender)); + + assertEquals(OK, ((GlideClusterClient) sender).publish(message, channel, true).get()); + + // Allow the message to propagate. + Thread.sleep(MESSAGE_DELIVERY_DELAY); + + assertEquals(1, callbackMessages.size()); + assertEquals(message, callbackMessages.get(0).getMessage()); + } + + /** Test the behavior if the callback supplied to a subscription throws an uncaught exception. */ + @SuppressWarnings("unchecked") + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void pubsub_test_callback_exception(boolean standalone) { + final GlideString channel = gs(UUID.randomUUID().toString()); + final GlideString message1 = gs("message1"); + final GlideString message2 = gs("message2"); + final GlideString errorMsg = gs("errorMsg"); + final GlideString message3 = gs("message3"); + + ArrayList callbackMessages = new ArrayList<>(); + final MessageCallback callback = + (pubSubMessage, context) -> { + if (pubSubMessage.getMessage().equals(errorMsg)) { + throw new RuntimeException("Test callback error message"); + } + ArrayList receivedMessages = (ArrayList) context; + receivedMessages.add(pubSubMessage); + }; + + Map> subscriptions = + standalone + ? Map.of(PubSubChannelMode.EXACT, Set.of(channel)) + : Map.of(PubSubClusterChannelMode.EXACT, Set.of(channel)); + + var listener = + createClientWithSubscriptions( + standalone, + subscriptions, + Optional.ofNullable(callback), + Optional.of(callbackMessages)); + var sender = createClient(standalone); + clients.addAll(Arrays.asList(listener, sender)); + + assertEquals(OK, sender.publish(message1, channel).get()); + assertEquals(OK, sender.publish(message2, channel).get()); + assertEquals(OK, sender.publish(errorMsg, channel).get()); + assertEquals(OK, sender.publish(message3, channel).get()); + + // Allow the message to propagate. + Thread.sleep(MESSAGE_DELIVERY_DELAY); + + assertEquals(3, callbackMessages.size()); + assertEquals(message1, callbackMessages.get(0).getMessage()); + assertEquals(channel, callbackMessages.get(0).getChannel()); + assertTrue(callbackMessages.get(0).getPattern().isEmpty()); + + assertEquals(message2, callbackMessages.get(1).getMessage()); + assertEquals(channel, callbackMessages.get(1).getChannel()); + assertTrue(callbackMessages.get(1).getPattern().isEmpty()); + + // Ensure we can receive message 3 which is after the message that triggers a throw. + assertEquals(message3, callbackMessages.get(2).getMessage()); + assertEquals(channel, callbackMessages.get(2).getChannel()); + assertTrue(callbackMessages.get(2).getPattern().isEmpty()); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + @ParameterizedTest(name = "standalone = {0}") + @ValueSource(booleans = {true, false}) + public void pubsub_with_binary(boolean standalone) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString channel = gs(new byte[] {(byte) 0xE2, 0x28, (byte) 0xA1}); + var message = + new PubSubMessage(gs(new byte[] {(byte) 0xF0, 0x28, (byte) 0x8C, (byte) 0xBC}), channel); + + ArrayList callbackMessages = new ArrayList<>(); + final MessageCallback callback = + (pubSubMessage, context) -> { + ArrayList receivedMessages = (ArrayList) context; + receivedMessages.add(pubSubMessage); + }; + + Map> subscriptions = + standalone + ? Map.of(PubSubChannelMode.EXACT, Set.of(channel)) + : Map.of(PubSubClusterChannelMode.EXACT, Set.of(channel)); + + var listener = createClientWithSubscriptions(standalone, subscriptions); + var listener2 = + createClientWithSubscriptions( + standalone, subscriptions, Optional.of(callback), Optional.of(callbackMessages)); + var sender = createClient(standalone); + clients.addAll(Arrays.asList(listener, listener2, sender)); + + assertEquals(OK, sender.publish(message.getMessage(), channel).get()); + Thread.sleep(MESSAGE_DELIVERY_DELAY); // deliver the messages + + assertEquals(message, listener.tryGetPubSubMessage()); + assertEquals(1, callbackMessages.size()); + assertEquals(message, callbackMessages.get(0)); + } +} diff --git a/java/integTest/src/test/java/glide/SharedClientTests.java b/java/integTest/src/test/java/glide/SharedClientTests.java index c120a7d1b9..bf106f1ff4 100644 --- a/java/integTest/src/test/java/glide/SharedClientTests.java +++ b/java/integTest/src/test/java/glide/SharedClientTests.java @@ -1,4 +1,4 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; import static glide.TestUtilities.commonClientConfig; @@ -8,8 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import glide.api.BaseClient; -import glide.api.RedisClient; -import glide.api.RedisClusterClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -28,17 +28,17 @@ @Timeout(25) // seconds public class SharedClientTests { - private static RedisClient standaloneClient = null; - private static RedisClusterClient clusterClient = null; + private static GlideClient standaloneClient = null; + private static GlideClusterClient clusterClient = null; @Getter private static List clients; @BeforeAll @SneakyThrows public static void init() { - standaloneClient = RedisClient.CreateClient(commonClientConfig().build()).get(); + standaloneClient = GlideClient.createClient(commonClientConfig().build()).get(); clusterClient = - RedisClusterClient.CreateClient(commonClusterClientConfig().requestTimeout(10000).build()) + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) .get(); clients = List.of(Arguments.of(standaloneClient), Arguments.of(clusterClient)); @@ -55,9 +55,9 @@ public static void teardown() { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void send_and_receive_large_values(BaseClient client) { - int length = 1 << 16; - String key = getRandomString(length); - String value = getRandomString(length); + int length = 1 << 25; // 33mb + String key = "0".repeat(length); + String value = "0".repeat(length); assertEquals(length, key.length()); assertEquals(length, value.length()); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 449ff122b7..3a93cb7939 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -1,14 +1,20 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; import static glide.TestConfiguration.CLUSTER_PORTS; -import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestConfiguration.SERVER_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.TestUtilities.assertDeepEquals; +import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.commonClusterClientConfig; import static glide.api.BaseClient.OK; +import static glide.api.models.GlideString.gs; import static glide.api.models.commands.LInsertOptions.InsertPosition.AFTER; import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; import static glide.api.models.commands.RangeOptions.InfScoreBound.NEGATIVE_INFINITY; import static glide.api.models.commands.RangeOptions.InfScoreBound.POSITIVE_INFINITY; +import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.ScoreFilter.MIN; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.Expiry.Milliseconds; @@ -16,16 +22,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import glide.api.BaseClient; -import glide.api.RedisClient; -import glide.api.RedisClusterClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; +import glide.api.models.GlideString; import glide.api.models.Script; +import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.GetExOptions; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; @@ -34,24 +47,80 @@ import glide.api.models.commands.RangeOptions.RangeByLex; import glide.api.models.commands.RangeOptions.RangeByScore; import glide.api.models.commands.RangeOptions.ScoreBoundary; +import glide.api.models.commands.RestoreOptions; import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.ScriptOptionsGlideString; import glide.api.models.commands.SetOptions; -import glide.api.models.commands.StreamAddOptions; -import glide.api.models.commands.ZaddOptions; +import glide.api.models.commands.SortOrder; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.WeightAggregateOptions.KeyArrayBinary; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; +import glide.api.models.commands.WeightAggregateOptions.WeightedKeysBinary; +import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.geospatial.GeoAddOptions; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.CoordOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.MemberOrigin; +import glide.api.models.commands.geospatial.GeoSearchOrigin.MemberOriginBinary; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.HScanOptionsBinary; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.SScanOptionsBinary; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.scan.ZScanOptionsBinary; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamAddOptionsBinary; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamPendingOptions; +import glide.api.models.commands.stream.StreamPendingOptionsBinary; +import glide.api.models.commands.stream.StreamRange.IdBound; +import glide.api.models.commands.stream.StreamRange.InfRangeBound; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions.MaxLen; +import glide.api.models.commands.stream.StreamTrimOptions.MinId; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; -import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import lombok.Getter; import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -63,8 +132,8 @@ @Timeout(10) // seconds public class SharedCommandTests { - private static RedisClient standaloneClient = null; - private static RedisClusterClient clusterClient = null; + private static GlideClient standaloneClient = null; + private static GlideClusterClient clusterClient = null; @Getter private static List clients; @@ -76,16 +145,16 @@ public class SharedCommandTests { @SneakyThrows public static void init() { standaloneClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() + GlideClient.createClient( + GlideClientConfiguration.builder() .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build()) .requestTimeout(5000) .build()) .get(); clusterClient = - RedisClusterClient.CreateClient( - RedisClusterClientConfiguration.builder() + GlideClusterClient.createClient( + GlideClusterClientConfiguration.builder() .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build()) .requestTimeout(5000) .build()) @@ -121,6 +190,26 @@ public void unlink_multiple_keys(BaseClient client) { assertEquals(3L, unlinkedKeysNum); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void unlink_binary_multiple_keys(BaseClient client) { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + GlideString key3 = gs("{key}" + UUID.randomUUID()); + GlideString value = gs(UUID.randomUUID().toString()); + + String setResult = client.set(key1, value).get(); + assertEquals(OK, setResult); + setResult = client.set(key2, value).get(); + assertEquals(OK, setResult); + setResult = client.set(key3, value).get(); + assertEquals(OK, setResult); + + Long unlinkedKeysNum = client.unlink(new GlideString[] {key1, key2, key3}).get(); + assertEquals(3L, unlinkedKeysNum); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -148,6 +237,42 @@ public void get_missing_value(BaseClient client) { assertNull(data); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void append(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String value1 = String.valueOf(UUID.randomUUID()); + String key2 = UUID.randomUUID().toString(); + + // Append on non-existing string(similar to SET) + assertEquals(value1.length(), client.append(key1, value1).get()); + + assertEquals(value1.length() * 2L, client.append(key1, value1).get()); + assertEquals(value1.concat(value1), client.get(key1).get()); + + // key exists but holding the wrong kind of value + assertEquals(1, client.sadd(key2, new String[] {"a"}).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.append(key2, "z").get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void appendBinary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString value = gs(String.valueOf(UUID.randomUUID())); + + // Append on non-existing string(similar to SET) + assertEquals(value.getString().length(), client.append(key, value).get()); + + assertEquals(value.getString().length() * 2L, client.append(key, value).get()); + GlideString value2 = gs(value.getString() + value.getString()); + assertEquals(value2, client.get(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -168,6 +293,25 @@ public void del_multiple_keys(BaseClient client) { assertEquals(3L, deletedKeysNum); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void del_multiple_keys_binary(BaseClient client) { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + GlideString key3 = gs("{key}" + UUID.randomUUID()); + GlideString value = gs(UUID.randomUUID().toString()); + String setResult = client.set(key1, value).get(); + assertEquals(OK, setResult); + setResult = client.set(key2, value).get(); + assertEquals(OK, setResult); + setResult = client.set(key3, value).get(); + assertEquals(OK, setResult); + + Long deletedKeysNum = client.del(new GlideString[] {key1, key2, key3}).get(); + assertEquals(3L, deletedKeysNum); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -203,7 +347,123 @@ public void set_requires_a_key(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void get_requires_a_key(BaseClient client) { - assertThrows(NullPointerException.class, () -> client.get(null)); + assertThrows(NullPointerException.class, () -> client.get((String) null)); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void getdel(BaseClient client) { + String key1 = "{key}" + UUID.randomUUID(); + String value1 = String.valueOf(UUID.randomUUID()); + String key2 = "{key}" + UUID.randomUUID(); + + client.set(key1, value1).get(); + String data = client.getdel(key1).get(); + assertEquals(data, value1); + data = client.getdel(key1).get(); + assertNull(data); + assertNull(client.getdel(key2).get()); + + // key isn't a string + client.sadd(key2, new String[] {"a"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.getdel(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void getex(BaseClient client) { + + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + + String key1 = "{key}" + UUID.randomUUID(); + String value1 = String.valueOf(UUID.randomUUID()); + String key2 = "{key}" + UUID.randomUUID(); + + client.set(key1, value1).get(); + String data = client.getex(key1).get(); + assertEquals(data, value1); + assertEquals(-1, client.ttl(key1).get()); + + data = client.getex(key1, GetExOptions.Seconds(10L)).get(); + Long ttlValue = client.ttl(key1).get(); + assertTrue(ttlValue >= 0L); + + // non-existent key + data = client.getex(key2).get(); + assertNull(data); + + // key isn't a string + client.sadd(key2, new String[] {"a"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.getex(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // with option + data = client.getex(key1, GetExOptions.Seconds(10L)).get(); + assertEquals(data, value1); + + // invalid time measurement + ExecutionException invalidTimeException = + assertThrows( + ExecutionException.class, () -> client.getex(key1, GetExOptions.Seconds(-10L)).get()); + assertInstanceOf(RequestException.class, invalidTimeException.getCause()); + + // setting and clearing expiration timer + assertEquals(value1, client.getex(key1, GetExOptions.Seconds(10L)).get()); + assertEquals(value1, client.getex(key1, GetExOptions.Persist()).get()); + assertEquals(-1L, client.ttl(key1).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void getex_binary(BaseClient client) { + + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString value1 = gs(String.valueOf(UUID.randomUUID())); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + + client.set(key1, value1).get(); + GlideString data = client.getex(key1).get(); + assertEquals(data, value1); + assertEquals(-1, client.ttl(key1).get()); + + data = client.getex(key1, GetExOptions.Seconds(10L)).get(); + Long ttlValue = client.ttl(key1).get(); + assertTrue(ttlValue >= 0L); + + // non-existent key + data = client.getex(key2).get(); + assertNull(data); + + // key isn't a string + client.sadd(key2, new GlideString[] {gs("a")}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.getex(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // with option + data = client.getex(key1, GetExOptions.Seconds(10L)).get(); + assertEquals(data, value1); + + // invalid time measurement + ExecutionException invalidTimeException = + assertThrows( + ExecutionException.class, () -> client.getex(key1, GetExOptions.Seconds(-10L)).get()); + assertInstanceOf(RequestException.class, invalidTimeException.getCause()); + + // setting and clearing expiration timer + assertEquals(value1, client.getex(key1, GetExOptions.Seconds(10L)).get()); + assertEquals(value1, client.getex(key1, GetExOptions.Persist()).get()); + assertEquals(-1L, client.ttl(key1).get()); } @SneakyThrows @@ -240,6 +500,29 @@ public void set_only_if_does_not_exists_missing_key(BaseClient client) { assertEquals(ANOTHER_VALUE, data); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void set_get_binary_data(BaseClient client) { + GlideString key = gs("set_get_binary_data_key"); + byte[] binvalue = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + assertEquals(client.set(key, gs(binvalue)).get(), "OK"); + GlideString data = client.get(key).get(); + assertArrayEquals(data.getBytes(), binvalue); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void set_get_binary_data_with_options(BaseClient client) { + SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_DOES_NOT_EXIST).build(); + GlideString key = gs("set_get_binary_data_with_options"); + byte[] binvalue = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + assertEquals(client.set(key, gs(binvalue), options).get(), "OK"); + GlideString data = client.get(key).get(); + assertArrayEquals(data.getBytes(), binvalue); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -325,6 +608,7 @@ public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void mset_mget_existing_non_existing_key(BaseClient client) { + // keys are from different slots String key1 = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); String key3 = UUID.randomUUID().toString(); @@ -338,6 +622,41 @@ public void mset_mget_existing_non_existing_key(BaseClient client) { client.mget(new String[] {key1, key2, nonExisting, key3}).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void mset_mget_existing_non_existing_key_binary(BaseClient client) { + // keys are from different slots + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString key3 = gs(UUID.randomUUID().toString()); + GlideString nonExisting = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map keyValueMap = Map.of(key1, value, key2, value, key3, value); + + assertEquals(OK, client.msetBinary(keyValueMap).get()); + assertArrayEquals( + new GlideString[] {value, value, null, value}, + client.mget(new GlideString[] {key1, key2, nonExisting, key3}).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void mset_mget_binary(BaseClient client) { + // keys are from different slots + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString key3 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map keyValueMap = Map.of(key1, value, key2, value, key3, value); + + assertEquals(OK, client.msetBinary(keyValueMap).get()); + assertArrayEquals( + new GlideString[] {value, value, value}, + client.mget(new GlideString[] {key1, key2, key3}).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -356,6 +675,24 @@ public void incr_commands_existing_key(BaseClient client) { assertEquals("20.5", client.get(key).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void incr_binary_commands_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("10")).get()); + + assertEquals(11, client.incr(key).get()); + assertEquals(gs("11"), client.get(key).get()); + + assertEquals(15, client.incrBy(key, 4).get()); + assertEquals(gs("15"), client.get(key).get()); + + assertEquals(20.5, client.incrByFloat(key, 5.5).get()); + assertEquals(gs("20.5"), client.get(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -377,6 +714,27 @@ public void incr_commands_non_existing_key(BaseClient client) { assertEquals("0.5", client.get(key3).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void incr_binary_commands_non_existing_key(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString key3 = gs(UUID.randomUUID().toString()); + + assertNull(client.get(key1).get()); + assertEquals(1, client.incr(key1).get()); + assertEquals(gs("1"), client.get(key1).get()); + + assertNull(client.get(key2).get()); + assertEquals(3, client.incrBy(key2, 3).get()); + assertEquals(gs("3"), client.get(key2).get()); + + assertNull(client.get(key3).get()); + assertEquals(0.5, client.incrByFloat(key3, 0.5).get()); + assertEquals(gs("0.5"), client.get(key3).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -412,6 +770,21 @@ public void decr_and_decrBy_existing_key(BaseClient client) { assertEquals("5", client.get(key).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void decr_and_decrBy_existing_key_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("10")).get()); + + assertEquals(9, client.decr(key).get()); + assertEquals(gs("9"), client.get(key).get()); + + assertEquals(5, client.decrBy(key, 4).get()); + assertEquals(gs("5"), client.get(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -447,6 +820,22 @@ public void strlen(BaseClient client) { assertTrue(exception.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void decr_and_decrBy_non_existing_key_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + assertNull(client.get(key1).get()); + assertEquals(-1, client.decr(key1).get()); + assertEquals(gs("-1"), client.get(key1).get()); + + assertNull(client.get(key2).get()); + assertEquals(-3, client.decrBy(key2, 3).get()); + assertEquals(gs("-3"), client.get(key2).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -475,6 +864,35 @@ public void setrange(BaseClient client) { assertTrue(exception.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void setrange_binary(BaseClient client) { + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString nonStringKey = gs(UUID.randomUUID().toString()); + // new key + assertEquals(11L, client.setrange(stringKey, 0, gs("Hello world")).get()); + // existing key + assertEquals(11L, client.setrange(stringKey, 6, gs("GLIDE")).get()); + assertEquals(gs("Hello GLIDE"), client.get(stringKey).get()); + + // offset > len + assertEquals(20L, client.setrange(stringKey, 15, gs("GLIDE")).get()); + assertEquals(gs("Hello GLIDE\0\0\0\0GLIDE"), client.get(stringKey).get()); + + // non-string key + assertEquals(1, client.lpush(nonStringKey, new GlideString[] {gs("_")}).get()); + Exception exception = + assertThrows( + ExecutionException.class, () -> client.setrange(nonStringKey, 0, gs("_")).get()); + assertTrue(exception.getCause() instanceof RequestException); + exception = + assertThrows( + ExecutionException.class, + () -> client.setrange(stringKey, Integer.MAX_VALUE, gs("_")).get()); + assertTrue(exception.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -497,12 +915,13 @@ public void getrange(BaseClient client) { // a redis bug, fixed in version 8: https://github.com/redis/redis/issues/13207 assertEquals( - REDIS_VERSION.isLowerThan("8.0.0") ? "T" : "", + SERVER_VERSION.isLowerThan("8.0.0") ? "T" : "", client.getrange(stringKey, -200, -100).get()); // empty key (returning null isn't implemented) assertEquals( - REDIS_VERSION.isLowerThan("8.0.0") ? "" : null, client.getrange(nonStringKey, 0, -1).get()); + SERVER_VERSION.isLowerThan("8.0.0") ? "" : null, + client.getrange(nonStringKey, 0, -1).get()); // non-string key assertEquals(1, client.lpush(nonStringKey, new String[] {"_"}).get()); @@ -511,6 +930,43 @@ public void getrange(BaseClient client) { assertTrue(exception.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void getrange_binary(BaseClient client) { + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString nonStringKey = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(stringKey, gs("This is a string")).get()); + assertEquals(gs("This"), client.getrange(stringKey, 0, 3).get()); + assertEquals(gs("ing"), client.getrange(stringKey, -3, -1).get()); + assertEquals(gs("This is a string"), client.getrange(stringKey, 0, -1).get()); + + // out of range + assertEquals(gs("string"), client.getrange(stringKey, 10, 100).get()); + assertEquals(gs("This is a stri"), client.getrange(stringKey, -200, -3).get()); + assertEquals(gs(""), client.getrange(stringKey, 100, 200).get()); + + // incorrect range + assertEquals(gs(""), client.getrange(stringKey, -1, -3).get()); + + // a redis bug, fixed in version 8: https://github.com/redis/redis/issues/13207 + assertEquals( + gs(SERVER_VERSION.isLowerThan("8.0.0") ? "T" : ""), + client.getrange(stringKey, -200, -100).get()); + + // empty key (returning null isn't implemented) + assertEquals( + gs(SERVER_VERSION.isLowerThan("8.0.0") ? "" : null), + client.getrange(nonStringKey, 0, -1).get()); + + // non-string key + assertEquals(1, client.lpush(nonStringKey, new GlideString[] {gs("_")}).get()); + Exception exception = + assertThrows(ExecutionException.class, () -> client.getrange(nonStringKey, 0, -1).get()); + assertTrue(exception.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -527,6 +983,22 @@ public void hset_hget_existing_fields_non_existing_fields(BaseClient client) { assertNull(client.hget(key, "non_existing_field").get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hset_hget_binary_existing_fields_non_existing_fields(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map fieldValueMap = Map.of(field1, value, field2, value); + + assertEquals(2, client.hset(key, fieldValueMap).get()); + assertEquals(value, client.hget(key, field1).get()); + assertEquals(value, client.hget(key, field2).get()); + assertNull(client.hget(key, gs("non_existing_field")).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -546,6 +1018,25 @@ public void hsetnx(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hsetnx_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString field = gs(UUID.randomUUID().toString()); + + assertTrue(client.hsetnx(key1, field, gs("value")).get()); + assertFalse(client.hsetnx(key1, field, gs("newValue")).get()); + assertEquals(gs("value"), client.hget(key1, field).get()); + + // Key exists, but it is not a hash + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hsetnx(key2, field, gs("value")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -563,6 +1054,25 @@ public void hdel_multiple_existing_fields_non_existing_field_non_existing_key(Ba assertEquals(0, client.hdel("non_existing_key", new String[] {field3}).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hdel_multiple_existing_fields_non_existing_field_non_existing_key_binary( + BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString field3 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map fieldValueMap = + Map.of(field1, value, field2, value, field3, value); + + assertEquals(3, client.hset(key, fieldValueMap).get()); + assertEquals(2, client.hdel(key, new GlideString[] {field1, field2}).get()); + assertEquals(0, client.hdel(key, new GlideString[] {gs("non_existing_field")}).get()); + assertEquals(0, client.hdel(gs("non_existing_key"), new GlideString[] {field3}).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -587,6 +1097,30 @@ public void hlen(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hlen_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map fieldValueMap = Map.of(field1, value, field2, value); + + assertEquals(2, client.hset(key1, fieldValueMap).get()); + assertEquals(2, client.hlen(key1).get()); + assertEquals(1, client.hdel(key1, new GlideString[] {field1}).get()); + assertEquals(1, client.hlen(key1).get()); + assertEquals(0, client.hlen(gs("nonExistingHash")).get()); + + // Key exists, but it is not a hash + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hlen(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -616,16 +1150,43 @@ public void hvals(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void hmget_multiple_existing_fields_non_existing_field_non_existing_key( - BaseClient client) { - String key = UUID.randomUUID().toString(); - String field1 = UUID.randomUUID().toString(); - String field2 = UUID.randomUUID().toString(); - String value = UUID.randomUUID().toString(); - Map fieldValueMap = Map.of(field1, value, field2, value); + public void hvals_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + Map fieldValueMap = + Map.of(field1, gs("value1"), field2, gs("value2")); - assertEquals(2, client.hset(key, fieldValueMap).get()); - assertArrayEquals( + assertEquals(2, client.hset(key1, fieldValueMap).get()); + + GlideString[] hvalsPayload = client.hvals(key1).get(); + Arrays.sort(hvalsPayload); // ordering for values by hvals is not guaranteed + assertArrayEquals(new GlideString[] {gs("value1"), gs("value2")}, hvalsPayload); + + assertEquals(1, client.hdel(key1, new GlideString[] {field1}).get()); + assertArrayEquals(new GlideString[] {gs("value2")}, client.hvals(key1).get()); + assertArrayEquals(new GlideString[] {}, client.hvals(gs("nonExistingKey")).get()); + + assertEquals(OK, client.set(key2, gs("value2")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hvals(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hmget_multiple_existing_fields_non_existing_field_non_existing_key( + BaseClient client) { + String key = UUID.randomUUID().toString(); + String field1 = UUID.randomUUID().toString(); + String field2 = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + Map fieldValueMap = Map.of(field1, value, field2, value); + + assertEquals(2, client.hset(key, fieldValueMap).get()); + assertArrayEquals( new String[] {value, null, value}, client.hmget(key, new String[] {field1, "non_existing_field", field2}).get()); assertArrayEquals( @@ -633,6 +1194,26 @@ public void hmget_multiple_existing_fields_non_existing_field_non_existing_key( client.hmget("non_existing_key", new String[] {field1, field2}).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hmget_binary_multiple_existing_fields_non_existing_field_non_existing_key( + BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map fieldValueMap = Map.of(field1, value, field2, value); + + assertEquals(2, client.hset(key, fieldValueMap).get()); + assertArrayEquals( + new GlideString[] {value, null, value}, + client.hmget(key, new GlideString[] {field1, gs("non_existing_field"), field2}).get()); + assertArrayEquals( + new GlideString[] {null, null}, + client.hmget(gs("non_existing_key"), new GlideString[] {field1, field2}).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -648,6 +1229,22 @@ public void hexists_existing_field_non_existing_field_non_existing_key(BaseClien assertFalse(client.hexists("non_existing_key", field2).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hexists_binary_existing_field_non_existing_field_non_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + Map fieldValueMap = + Map.of(field1, gs("value1"), field2, gs("value1")); + + assertEquals(2, client.hset(key, fieldValueMap).get()); + assertTrue(client.hexists(key, field1).get()); + assertFalse(client.hexists(key, gs("non_existing_field")).get()); + assertFalse(client.hexists(gs("non_existing_key"), field2).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -663,6 +1260,23 @@ public void hgetall_multiple_existing_fields_existing_key_non_existing_key(BaseC assertEquals(Map.of(), client.hgetall("non_existing_key").get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hgetall_binary_api(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + HashMap fieldValueMap = + new HashMap<>(Map.of(field1, value, field2, value)); + + assertEquals(2, client.hset(key, fieldValueMap).get()); + Map allItems = client.hgetall(key).get(); + assertEquals(value, allItems.get(field1)); + assertEquals(value, allItems.get(field2)); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -715,6 +1329,188 @@ public void hincrBy_hincrByFloat_type_error(BaseClient client) { assertTrue(hincrByFloatException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hkeys(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + var data = new LinkedHashMap(); + data.put("f 1", "v 1"); + data.put("f 2", "v 2"); + assertEquals(2, client.hset(key1, data).get()); + assertArrayEquals(new String[] {"f 1", "f 2"}, client.hkeys(key1).get()); + + assertEquals(0, client.hkeys(key2).get().length); + + // Key exists, but it is not a hash + assertEquals(OK, client.set(key2, "value").get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.hkeys(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hkeys_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + var data = new LinkedHashMap(); + data.put(gs("f 1"), gs("v 1")); + data.put(gs("f 2"), gs("v 2")); + assertEquals(2, client.hset(key1, data).get()); + assertArrayEquals(new GlideString[] {gs("f 1"), gs("f 2")}, client.hkeys(key1).get()); + + assertEquals(0, client.hkeys(key2).get().length); + + // Key exists, but it is not a hash + assertEquals(OK, client.set(key2, gs("value")).get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.hkeys(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hstrlen(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + assertEquals(1, client.hset(key1, Map.of("field", "value")).get()); + assertEquals(5L, client.hstrlen(key1, "field").get()); + + // missing value + assertEquals(0, client.hstrlen(key1, "field 2").get()); + + // missing key + assertEquals(0, client.hstrlen(key2, "field").get()); + + // Key exists, but it is not a hash + assertEquals(OK, client.set(key2, "value").get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.hstrlen(key2, "field").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hrandfield(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + // key does not exist + assertNull(client.hrandfield(key1).get()); + assertEquals(0, client.hrandfieldWithCount(key1, 5).get().length); + assertEquals(0, client.hrandfieldWithCountWithValues(key1, 5).get().length); + + var data = Map.of("f 1", "v 1", "f 2", "v 2", "f 3", "v 3"); + assertEquals(3, client.hset(key1, data).get()); + + // random key + assertTrue(data.containsKey(client.hrandfield(key1).get())); + + // WithCount - positive count + var keys = client.hrandfieldWithCount(key1, 5).get(); + assertEquals(data.keySet().size(), keys.length); + assertEquals(data.keySet(), Set.of(keys)); + + // WithCount - negative count + keys = client.hrandfieldWithCount(key1, -5).get(); + assertEquals(5, keys.length); + Arrays.stream(keys).forEach(key -> assertTrue(data.containsKey(key))); + + // WithCountWithValues - positive count + var keysWithValues = client.hrandfieldWithCountWithValues(key1, 5).get(); + assertEquals(data.keySet().size(), keysWithValues.length); + for (var pair : keysWithValues) { + assertEquals(data.get(pair[0]), pair[1]); + } + + // WithCountWithValues - negative count + keysWithValues = client.hrandfieldWithCountWithValues(key1, -5).get(); + assertEquals(5, keysWithValues.length); + for (var pair : keysWithValues) { + assertEquals(data.get(pair[0]), pair[1]); + } + + // Key exists, but it is not a List + assertEquals(OK, client.set(key2, "value").get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.hrandfield(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.hrandfieldWithCount(key2, 2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.hrandfieldWithCountWithValues(key2, 3).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hrandfieldBinary(BaseClient client) { + byte[] binvalue1 = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + byte[] binvalue2 = {(byte) 0xFF, (byte) 0x66, (byte) 0xFF, (byte) 0xAF, (byte) 0x22}; + + GlideString key1 = gs(binvalue1); + GlideString key2 = gs(binvalue2); + + // key does not exist + assertNull(client.hrandfield(key1).get()); + assertEquals(0, client.hrandfieldWithCount(key1, 5).get().length); + assertEquals(0, client.hrandfieldWithCountWithValues(key1, 5).get().length); + + var data = Map.of(gs("f 1"), gs("v 1"), gs("f 2"), gs("v 2"), gs("f 3"), gs("v 3")); + assertEquals(3, client.hset(key1, data).get()); + + // random key + assertTrue(data.containsKey(client.hrandfield(key1).get())); + + // WithCount - positive count + var keys = client.hrandfieldWithCount(key1, 5).get(); + assertEquals(data.keySet().size(), keys.length); + assertEquals(data.keySet(), Set.of(keys)); + + // WithCount - negative count + keys = client.hrandfieldWithCount(key1, -5).get(); + assertEquals(5, keys.length); + Arrays.stream(keys).forEach(key -> assertTrue(data.containsKey(key))); + + // WithCountWithValues - positive count + var keysWithValues = client.hrandfieldWithCountWithValues(key1, 5).get(); + assertEquals(data.keySet().size(), keysWithValues.length); + for (var pair : keysWithValues) { + assertEquals(data.get(pair[0]), pair[1]); + } + + // WithCountWithValues - negative count + keysWithValues = client.hrandfieldWithCountWithValues(key1, -5).get(); + assertEquals(5, keysWithValues.length); + for (var pair : keysWithValues) { + assertEquals(data.get(pair[0]), pair[1]); + } + + // Key exists, but it is not a List + assertEquals(OK, client.set(key2, gs("value")).get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.hrandfield(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.hrandfieldWithCount(key2, 2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.hrandfieldWithCountWithValues(key2, 3).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -730,6 +1526,25 @@ public void lpush_lpop_lrange_existing_non_existing_key(BaseClient client) { assertNull(client.lpop("non_existing_key").get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lpush_lpop_lrange_binary_existing_non_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString[] valueArray = + new GlideString[] {gs("value4"), gs("value3"), gs("value2"), gs("value1")}; + + assertEquals(4, client.lpush(key, valueArray).get()); + assertEquals(gs("value1"), client.lpop(key).get()); + assertArrayEquals( + new GlideString[] {gs("value2"), gs("value3"), gs("value4")}, + client.lrange(key, 0, -1).get()); + assertArrayEquals( + new GlideString[] {gs("value2"), gs("value3")}, client.lpopCount(key, 2).get()); + assertArrayEquals(new GlideString[] {}, client.lrange(gs("non_existing_key"), 0, -1).get()); + assertNull(client.lpop(gs("non_existing_key")).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -754,6 +1569,31 @@ public void lpush_lpop_lrange_type_error(BaseClient client) { assertTrue(lrangeException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lpush_lpop_lrange_binary_type_error(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("foo")).get()); + + Exception lpushException = + assertThrows( + ExecutionException.class, () -> client.lpush(key, new GlideString[] {gs("foo")}).get()); + assertTrue(lpushException.getCause() instanceof RequestException); + + Exception lpopException = assertThrows(ExecutionException.class, () -> client.lpop(key).get()); + assertTrue(lpopException.getCause() instanceof RequestException); + + Exception lpopCountException = + assertThrows(ExecutionException.class, () -> client.lpopCount(key, 2).get()); + assertTrue(lpopCountException.getCause() instanceof RequestException); + + Exception lrangeException = + assertThrows(ExecutionException.class, () -> client.lrange(key, 0, -1).get()); + assertTrue(lrangeException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -775,6 +1615,27 @@ public void lindex(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lindex_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString[] valueArray = new GlideString[] {gs("value1"), gs("value2")}; + + assertEquals(2, client.lpush(key1, valueArray).get()); + assertEquals(valueArray[1], client.lindex(key1, 0).get()); + assertEquals(valueArray[0], client.lindex(key1, -1).get()); + assertNull(client.lindex(key1, 3).get()); + assertNull(client.lindex(key2, 3).get()); + + // Key exists, but it is not a List + assertEquals(OK, client.set(key2, gs("value")).get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.lindex(key2, 0).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -797,6 +1658,30 @@ public void ltrim_existing_non_existing_key_and_type_error(BaseClient client) { assertTrue(ltrimException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void ltrim_binary_existing_non_existing_key_and_type_error(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString[] valueArray = + new GlideString[] {gs("value4"), gs("value3"), gs("value2"), gs("value1")}; + + assertEquals(4, client.lpush(key, valueArray).get()); + assertEquals(OK, client.ltrim(key, 0, 1).get()); + assertArrayEquals( + new GlideString[] {gs("value1"), gs("value2")}, client.lrange(key, 0, -1).get()); + + // `start` is greater than `end` so the key will be removed. + assertEquals(OK, client.ltrim(key, 4, 2).get()); + assertArrayEquals(new GlideString[] {}, client.lrange(key, 0, -1).get()); + + assertEquals(OK, client.set(key, gs("foo")).get()); + + Exception ltrimException = + assertThrows(ExecutionException.class, () -> client.ltrim(key, 0, 1).get()); + assertTrue(ltrimException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -843,62 +1728,314 @@ public void lrem_existing_non_existing_key_and_type_error(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void rpush_rpop_existing_non_existing_key(BaseClient client) { - String key = UUID.randomUUID().toString(); - String[] valueArray = new String[] {"value1", "value2", "value3", "value4"}; + public void lpos(BaseClient client) { + String key = "{ListKey}-1-" + UUID.randomUUID(); + String[] valueArray = new String[] {"a", "a", "b", "c", "a", "b"}; + assertEquals(6L, client.rpush(key, valueArray).get()); - assertEquals(4, client.rpush(key, valueArray).get()); - assertEquals("value4", client.rpop(key).get()); + // simplest case + assertEquals(0L, client.lpos(key, "a").get()); + assertEquals(5L, client.lpos(key, "b", LPosOptions.builder().rank(2L).build()).get()); - assertArrayEquals(new String[] {"value3", "value2"}, client.rpopCount(key, 2).get()); - assertNull(client.rpop("non_existing_key").get()); - } + // element doesn't exist + assertNull(client.lpos(key, "e").get()); - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void rpush_rpop_type_error(BaseClient client) { - String key = UUID.randomUUID().toString(); + // reverse traversal + assertEquals(2L, client.lpos(key, "b", LPosOptions.builder().rank(-2L).build()).get()); - assertEquals(OK, client.set(key, "foo").get()); + // unlimited comparisons + assertEquals( + 0L, client.lpos(key, "a", LPosOptions.builder().rank(1L).maxLength(0L).build()).get()); - Exception rpushException = - assertThrows(ExecutionException.class, () -> client.rpush(key, new String[] {"foo"}).get()); - assertTrue(rpushException.getCause() instanceof RequestException); + // limited comparisons + assertNull(client.lpos(key, "c", LPosOptions.builder().rank(1L).maxLength(2L).build()).get()); - Exception rpopException = assertThrows(ExecutionException.class, () -> client.rpop(key).get()); - assertTrue(rpopException.getCause() instanceof RequestException); + // invalid rank value + ExecutionException lposException = + assertThrows( + ExecutionException.class, + () -> client.lpos(key, "a", LPosOptions.builder().rank(0L).build()).get()); + assertTrue(lposException.getCause() instanceof RequestException); + + // invalid maxlen value + ExecutionException lposMaxlenException = + assertThrows( + ExecutionException.class, + () -> client.lpos(key, "a", LPosOptions.builder().maxLength(-1L).build()).get()); + assertTrue(lposMaxlenException.getCause() instanceof RequestException); + + // non-existent key + assertNull(client.lpos("non-existent_key", "a").get()); + + // wrong key data type + String wrong_data_type = "key" + UUID.randomUUID(); + assertEquals(2L, client.sadd(wrong_data_type, new String[] {"a", "b"}).get()); + ExecutionException lposWrongKeyDataTypeException = + assertThrows(ExecutionException.class, () -> client.lpos(wrong_data_type, "a").get()); + assertTrue(lposWrongKeyDataTypeException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void sadd_srem_scard_smembers_existing_set(BaseClient client) { - String key = UUID.randomUUID().toString(); + public void lpos_binary(BaseClient client) { + GlideString key = gs("{ListKey}-1-" + UUID.randomUUID()); + GlideString[] valueArray = + new GlideString[] {gs("a"), gs("a"), gs("b"), gs("c"), gs("a"), gs("b")}; + assertEquals(6L, client.rpush(key, valueArray).get()); + + // simplest case + assertEquals(0L, client.lpos(key, gs("a")).get()); + assertEquals(5L, client.lpos(key, gs("b"), LPosOptions.builder().rank(2L).build()).get()); + + // element doesn't exist + assertNull(client.lpos(key, gs("e")).get()); + + // reverse traversal + assertEquals(2L, client.lpos(key, gs("b"), LPosOptions.builder().rank(-2L).build()).get()); + + // unlimited comparisons assertEquals( - 4, client.sadd(key, new String[] {"member1", "member2", "member3", "member4"}).get()); - assertEquals(1, client.srem(key, new String[] {"member3", "nonExistingMember"}).get()); + 0L, client.lpos(key, gs("a"), LPosOptions.builder().rank(1L).maxLength(0L).build()).get()); - Set expectedMembers = Set.of("member1", "member2", "member4"); - assertEquals(expectedMembers, client.smembers(key).get()); - assertEquals(1, client.srem(key, new String[] {"member1"}).get()); - assertEquals(2, client.scard(key).get()); + // limited comparisons + assertNull( + client.lpos(key, gs("c"), LPosOptions.builder().rank(1L).maxLength(2L).build()).get()); + + // invalid rank value + ExecutionException lposException = + assertThrows( + ExecutionException.class, + () -> client.lpos(key, gs("a"), LPosOptions.builder().rank(0L).build()).get()); + assertTrue(lposException.getCause() instanceof RequestException); + + // invalid maxlen value + ExecutionException lposMaxlenException = + assertThrows( + ExecutionException.class, + () -> client.lpos(key, gs("a"), LPosOptions.builder().maxLength(-1L).build()).get()); + assertTrue(lposMaxlenException.getCause() instanceof RequestException); + + // non-existent key + assertNull(client.lpos(gs("non-existent_key"), gs("a")).get()); + + // wrong key data type + GlideString wrong_data_type = gs("key" + UUID.randomUUID()); + assertEquals(2L, client.sadd(wrong_data_type, new GlideString[] {gs("a"), gs("b")}).get()); + ExecutionException lposWrongKeyDataTypeException = + assertThrows(ExecutionException.class, () -> client.lpos(wrong_data_type, gs("a")).get()); + assertTrue(lposWrongKeyDataTypeException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void srem_scard_smembers_non_existing_key(BaseClient client) { - assertEquals(0, client.srem("nonExistingKey", new String[] {"member"}).get()); - assertEquals(0, client.scard("nonExistingKey").get()); - assertEquals(Set.of(), client.smembers("nonExistingKey").get()); + public void lposCount(BaseClient client) { + String key = "{ListKey}-1-" + UUID.randomUUID(); + String[] valueArray = new String[] {"a", "a", "b", "c", "a", "b"}; + assertEquals(6L, client.rpush(key, valueArray).get()); + + assertArrayEquals(new Long[] {0L, 1L}, client.lposCount(key, "a", 2L).get()); + assertArrayEquals(new Long[] {0L, 1L, 4L}, client.lposCount(key, "a", 0L).get()); + + // invalid count value + ExecutionException lposCountException = + assertThrows(ExecutionException.class, () -> client.lposCount(key, "a", -1L).get()); + assertTrue(lposCountException.getCause() instanceof RequestException); + + // with option + assertArrayEquals( + new Long[] {0L, 1L, 4L}, + client.lposCount(key, "a", 0L, LPosOptions.builder().rank(1L).build()).get()); + assertArrayEquals( + new Long[] {1L, 4L}, + client.lposCount(key, "a", 0L, LPosOptions.builder().rank(2L).build()).get()); + assertArrayEquals( + new Long[] {4L}, + client.lposCount(key, "a", 0L, LPosOptions.builder().rank(3L).build()).get()); + + // reverse traversal + assertArrayEquals( + new Long[] {4L, 1L, 0L}, + client.lposCount(key, "a", 0L, LPosOptions.builder().rank(-1L).build()).get()); + + // non-existent key + assertArrayEquals(new Long[] {}, client.lposCount("non-existent_key", "a", 1L).get()); + + // wrong key data type + String wrong_data_type = "key" + UUID.randomUUID(); + assertEquals(2L, client.sadd(wrong_data_type, new String[] {"a", "b"}).get()); + ExecutionException lposWrongKeyDataTypeException = + assertThrows( + ExecutionException.class, () -> client.lposCount(wrong_data_type, "a", 1L).get()); + assertTrue(lposWrongKeyDataTypeException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void sadd_srem_scard_smembers_key_with_non_set_value(BaseClient client) { - String key = UUID.randomUUID().toString(); + public void lposCount_binary(BaseClient client) { + GlideString key = gs("{ListKey}-1-" + UUID.randomUUID()); + GlideString[] valueArray = + new GlideString[] {gs("a"), gs("a"), gs("b"), gs("c"), gs("a"), gs("b")}; + assertEquals(6L, client.rpush(key, valueArray).get()); + + assertArrayEquals(new Long[] {0L, 1L}, client.lposCount(key, gs("a"), 2L).get()); + assertArrayEquals(new Long[] {0L, 1L, 4L}, client.lposCount(key, gs("a"), 0L).get()); + + // invalid count value + ExecutionException lposCountException = + assertThrows(ExecutionException.class, () -> client.lposCount(key, gs("a"), -1L).get()); + assertTrue(lposCountException.getCause() instanceof RequestException); + + // with option + assertArrayEquals( + new Long[] {0L, 1L, 4L}, + client.lposCount(key, gs("a"), 0L, LPosOptions.builder().rank(1L).build()).get()); + assertArrayEquals( + new Long[] {1L, 4L}, + client.lposCount(key, gs("a"), 0L, LPosOptions.builder().rank(2L).build()).get()); + assertArrayEquals( + new Long[] {4L}, + client.lposCount(key, gs("a"), 0L, LPosOptions.builder().rank(3L).build()).get()); + + // reverse traversal + assertArrayEquals( + new Long[] {4L, 1L, 0L}, + client.lposCount(key, gs("a"), 0L, LPosOptions.builder().rank(-1L).build()).get()); + + // non-existent key + assertArrayEquals(new Long[] {}, client.lposCount(gs("non-existent_key"), gs("a"), 1L).get()); + + // wrong key data type + GlideString wrong_data_type = gs("key" + UUID.randomUUID()); + assertEquals(2L, client.sadd(wrong_data_type, new GlideString[] {gs("a"), gs("b")}).get()); + ExecutionException lposWrongKeyDataTypeException = + assertThrows( + ExecutionException.class, () -> client.lposCount(wrong_data_type, gs("a"), 1L).get()); + assertTrue(lposWrongKeyDataTypeException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rpush_rpop_existing_non_existing_key(BaseClient client) { + String key = UUID.randomUUID().toString(); + String[] valueArray = new String[] {"value1", "value2", "value3", "value4"}; + + assertEquals(4, client.rpush(key, valueArray).get()); + assertEquals("value4", client.rpop(key).get()); + + assertArrayEquals(new String[] {"value3", "value2"}, client.rpopCount(key, 2).get()); + assertNull(client.rpop("non_existing_key").get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rpush_rpop_binary_existing_non_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString[] valueArray = + new GlideString[] {gs("value1"), gs("value2"), gs("value3"), gs("value4")}; + + assertEquals(4, client.rpush(key, valueArray).get()); + assertEquals(gs("value4"), client.rpop(key).get()); + + assertArrayEquals( + new GlideString[] {gs("value3"), gs("value2")}, client.rpopCount(key, 2).get()); + assertNull(client.rpop(gs("non_existing_key")).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rpush_rpop_type_error(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(key, "foo").get()); + + Exception rpushException = + assertThrows(ExecutionException.class, () -> client.rpush(key, new String[] {"foo"}).get()); + assertTrue(rpushException.getCause() instanceof RequestException); + + Exception rpopException = assertThrows(ExecutionException.class, () -> client.rpop(key).get()); + assertTrue(rpopException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rpush_rpop_binary_type_error(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("foo")).get()); + + Exception rpushException = + assertThrows( + ExecutionException.class, () -> client.rpush(key, new GlideString[] {gs("foo")}).get()); + assertTrue(rpushException.getCause() instanceof RequestException); + + Exception rpopException = assertThrows(ExecutionException.class, () -> client.rpop(key).get()); + assertTrue(rpopException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sadd_srem_scard_smembers_existing_set(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals( + 4, client.sadd(key, new String[] {"member1", "member2", "member3", "member4"}).get()); + assertEquals(1, client.srem(key, new String[] {"member3", "nonExistingMember"}).get()); + + Set expectedMembers = Set.of("member1", "member2", "member4"); + assertEquals(expectedMembers, client.smembers(key).get()); + + Set expectedMembersBin = Set.of("member1", "member2", "member4"); + assertEquals(expectedMembersBin, client.smembers(key).get()); + + assertEquals(1, client.srem(key, new String[] {"member1"}).get()); + assertEquals(2, client.scard(key).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sadd_srem_scard_smembers_binary_existing_set(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + assertEquals( + 4, + client + .sadd( + key, new GlideString[] {gs("member1"), gs("member2"), gs("member3"), gs("member4")}) + .get()); + assertEquals( + 1, client.srem(key, new GlideString[] {gs("member3"), gs("nonExistingMember")}).get()); + + Set expectedMembers = Set.of(gs("member1"), gs("member2"), gs("member4")); + assertEquals(expectedMembers, client.smembers(key).get()); + + Set expectedMembersBin = Set.of(gs("member1"), gs("member2"), gs("member4")); + assertEquals(expectedMembersBin, client.smembers(key).get()); + + assertEquals(1, client.srem(key, new GlideString[] {gs("member1")}).get()); + assertEquals(2, client.scard(key).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void srem_scard_smembers_non_existing_key(BaseClient client) { + assertEquals(0, client.srem("nonExistingKey", new String[] {"member"}).get()); + assertEquals(0, client.scard("nonExistingKey").get()); + assertEquals(Set.of(), client.smembers("nonExistingKey").get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sadd_srem_scard_smembers_key_with_non_set_value(BaseClient client) { + String key = UUID.randomUUID().toString(); assertEquals(OK, client.set(key, "foo").get()); Exception e = @@ -965,14 +2102,152 @@ public void smove(BaseClient client) { executionException = assertThrows(ExecutionException.class, () -> client.smove(setKey1, nonSetKey, "_").get()); assertInstanceOf(RequestException.class, executionException.getCause()); + } - // same-slot requirement - if (client instanceof RedisClusterClient) { - executionException = - assertThrows(ExecutionException.class, () -> client.smove("abc", "zxy", "lkn").get()); - assertInstanceOf(RequestException.class, executionException.getCause()); - assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); - } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void smove_binary(BaseClient client) { + GlideString setKey1 = gs("{key}" + UUID.randomUUID()); + GlideString setKey2 = gs("{key}" + UUID.randomUUID()); + GlideString setKey3 = gs("{key}" + UUID.randomUUID()); + GlideString nonSetKey = gs("{key}" + UUID.randomUUID()); + + assertEquals(3, client.sadd(setKey1, new GlideString[] {gs("1"), gs("2"), gs("3")}).get()); + assertEquals(2, client.sadd(setKey2, new GlideString[] {gs("2"), gs("3")}).get()); + + // move an elem + assertTrue(client.smove(setKey1, setKey2, gs("1")).get()); + assertEquals(Set.of(gs("2"), gs("3")), client.smembers(setKey1).get()); + assertEquals(Set.of(gs("1"), gs("2"), gs("3")), client.smembers(setKey2).get()); + + // move an elem which preset at destination + assertTrue(client.smove(setKey2, setKey1, gs("2")).get()); + assertEquals(Set.of(gs("2"), gs("3")), client.smembers(setKey1).get()); + assertEquals(Set.of(gs("1"), gs("3")), client.smembers(setKey2).get()); + + // move from missing key + assertFalse(client.smove(setKey3, setKey1, gs("4")).get()); + assertEquals(Set.of(gs("2"), gs("3")), client.smembers(setKey1).get()); + + // move to a new set + assertTrue(client.smove(setKey1, setKey3, gs("2")).get()); + assertEquals(Set.of(gs("3")), client.smembers(setKey1).get()); + assertEquals(Set.of(gs("2")), client.smembers(setKey3).get()); + + // move missing element + assertFalse(client.smove(setKey1, setKey3, gs("42")).get()); + assertEquals(Set.of(gs("3")), client.smembers(setKey1).get()); + assertEquals(Set.of(gs("2")), client.smembers(setKey3).get()); + + // move missing element to missing key + assertFalse(client.smove(setKey1, nonSetKey, gs("42")).get()); + assertEquals(Set.of(gs("3")), client.smembers(setKey1).get()); + assertEquals("none", client.type(nonSetKey).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(nonSetKey, gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.smove(nonSetKey, setKey1, gs("_")).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.smove(setKey1, nonSetKey, gs("_")).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rename(BaseClient client) { + String key1 = "{key}" + UUID.randomUUID(); + + assertEquals(OK, client.set(key1, "foo").get()); + assertEquals(OK, client.rename(key1, key1 + "_rename").get()); + assertEquals(1L, client.exists(new String[] {key1 + "_rename"}).get()); + + // key doesn't exist + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client.rename("{same_slot}" + "non_existing_key", "{same_slot}" + "_rename").get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rename_binary(BaseClient client) { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + + assertEquals(OK, client.set(key1, gs("foo")).get()); + assertEquals(OK, client.rename(key1, gs((key1.toString() + "_rename"))).get()); + assertEquals(1L, client.exists(new GlideString[] {gs(key1.toString() + "_rename")}).get()); + + // key doesn't exist + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .rename(gs("{same_slot}" + "non_existing_key"), gs("{same_slot}" + "_rename")) + .get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void renamenx(BaseClient client) { + String key1 = "{key}" + UUID.randomUUID(); + String key2 = "{key}" + UUID.randomUUID(); + String key3 = "{key}" + UUID.randomUUID(); + + assertEquals(OK, client.set(key3, "key3").get()); + + // rename missing key + var executionException = + assertThrows(ExecutionException.class, () -> client.renamenx(key1, key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().toLowerCase().contains("no such key")); + + // rename a string + assertEquals(OK, client.set(key1, "key1").get()); + assertTrue(client.renamenx(key1, key2).get()); + assertFalse(client.renamenx(key2, key3).get()); + assertEquals("key1", client.get(key2).get()); + assertEquals(1, client.del(new String[] {key1, key2}).get()); + + // this one remains unchanged + assertEquals("key3", client.get(key3).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void renamenx_binary(BaseClient client) { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + GlideString key3 = gs("{key}" + UUID.randomUUID()); + + assertEquals(OK, client.set(key3, gs("key3")).get()); + + // rename missing key + var executionException = + assertThrows(ExecutionException.class, () -> client.renamenx(key1, key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().toLowerCase().contains("no such key")); + + // rename a string + assertEquals(OK, client.set(key1, gs("key1")).get()); + assertTrue(client.renamenx(key1, key2).get()); + assertFalse(client.renamenx(key2, key3).get()); + assertEquals(gs("key1"), client.get(key2).get()); + assertEquals(1, client.del(new GlideString[] {key1, key2}).get()); + + // this one remains unchanged + assertEquals(gs("key3"), client.get(key3).get()); } @SneakyThrows @@ -994,6 +2269,25 @@ public void sismember(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sismember_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString member = gs(UUID.randomUUID().toString()); + + assertEquals(1, client.sadd(key1, new GlideString[] {member}).get()); + assertTrue(client.sismember(key1, member).get()); + assertFalse(client.sismember(key1, gs("nonExistingMember")).get()); + assertFalse(client.sismember(gs("nonExistingKey"), member).get()); + + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sismember(key2, member).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1044,65 +2338,196 @@ public void sinterstore(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void smismember(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); + public void sinterstore_binary(BaseClient client) { + GlideString key1 = gs("{key}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3-" + UUID.randomUUID()); + GlideString key4 = gs("{key}-4-" + UUID.randomUUID()); + GlideString key5 = gs("{key}-5-" + UUID.randomUUID()); - assertEquals(2, client.sadd(key1, new String[] {"one", "two"}).get()); - assertArrayEquals( - new Boolean[] {true, false}, client.smismember(key1, new String[] {"one", "three"}).get()); + assertEquals(3, client.sadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(3, client.sadd(key2, new GlideString[] {gs("c"), gs("d"), gs("e")}).get()); + assertEquals(3, client.sadd(key4, new GlideString[] {gs("e"), gs("f"), gs("g")}).get()); - // empty set - assertArrayEquals( - new Boolean[] {false, false}, client.smismember(key2, new String[] {"one", "three"}).get()); + // create new + assertEquals(1, client.sinterstore(key3, new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(gs("c")), client.smembers(key3).get()); - // Key exists, but it is not a set - assertEquals(OK, client.set(key2, "value").get()); + // overwrite existing set + assertEquals(1, client.sinterstore(key2, new GlideString[] {key3, key2}).get()); + assertEquals(Set.of(gs("c")), client.smembers(key2).get()); + + // overwrite source + assertEquals(0, client.sinterstore(key1, new GlideString[] {key1, key4}).get()); + assertEquals(Set.of(), client.smembers(key1).get()); + + // overwrite source + assertEquals(1, client.sinterstore(key2, new GlideString[] {key2}).get()); + assertEquals(Set.of(gs("c")), client.smembers(key2).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key5, gs("value")).get()); ExecutionException executionException = assertThrows( - ExecutionException.class, () -> client.smismember(key2, new String[] {"_"}).get()); - assertInstanceOf(RequestException.class, executionException.getCause()); + ExecutionException.class, + () -> client.sinterstore(key1, new GlideString[] {key5}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // overwrite destination - not a set + assertEquals(0, client.sinterstore(key5, new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(), client.smembers(key5).get()); + + // wrong arguments + executionException = + assertThrows( + ExecutionException.class, () -> client.sinterstore(key5, new GlideString[0]).get()); + assertTrue(executionException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void sdiffstore(BaseClient client) { + public void sdiff(BaseClient client) { String key1 = "{key}-1-" + UUID.randomUUID(); String key2 = "{key}-2-" + UUID.randomUUID(); String key3 = "{key}-3-" + UUID.randomUUID(); - String key4 = "{key}-4-" + UUID.randomUUID(); - String key5 = "{key}-5-" + UUID.randomUUID(); assertEquals(3, client.sadd(key1, new String[] {"a", "b", "c"}).get()); assertEquals(3, client.sadd(key2, new String[] {"c", "d", "e"}).get()); - assertEquals(3, client.sadd(key4, new String[] {"e", "f", "g"}).get()); - - // create new - assertEquals(2, client.sdiffstore(key3, new String[] {key1, key2}).get()); - assertEquals(Set.of("a", "b"), client.smembers(key3).get()); - // overwrite existing set - assertEquals(2, client.sdiffstore(key2, new String[] {key3, key2}).get()); - assertEquals(Set.of("a", "b"), client.smembers(key2).get()); - - // overwrite source - assertEquals(3, client.sdiffstore(key1, new String[] {key1, key4}).get()); - assertEquals(Set.of("a", "b", "c"), client.smembers(key1).get()); + assertEquals(Set.of("a", "b"), client.sdiff(new String[] {key1, key2}).get()); + assertEquals(Set.of("d", "e"), client.sdiff(new String[] {key2, key1}).get()); - // diff with empty set - assertEquals(3, client.sdiffstore(key1, new String[] {key1, key5}).get()); - assertEquals(Set.of("a", "b", "c"), client.smembers(key1).get()); + // second set is empty + assertEquals(Set.of("a", "b", "c"), client.sdiff(new String[] {key1, key3}).get()); - // diff empty with non-empty set - assertEquals(0, client.sdiffstore(key3, new String[] {key5, key1}).get()); - assertEquals(Set.of(), client.smembers(key3).get()); + // first set is empty + assertEquals(Set.of(), client.sdiff(new String[] {key3, key1}).get()); // source key exists, but it is not a set - assertEquals(OK, client.set(key5, "value").get()); + assertEquals(OK, client.set(key3, "value").get()); ExecutionException executionException = - assertThrows( - ExecutionException.class, () -> client.sdiffstore(key1, new String[] {key5}).get()); + assertThrows(ExecutionException.class, () -> client.sdiff(new String[] {key1, key3}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sdiff_gs(BaseClient client) { + GlideString key1 = gs("{key}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3-" + UUID.randomUUID()); + + assertEquals(3, client.sadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(3, client.sadd(key2, new GlideString[] {gs("c"), gs("d"), gs("e")}).get()); + + assertEquals(Set.of(gs("a"), gs("b")), client.sdiff(new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(gs("d"), gs("e")), client.sdiff(new GlideString[] {key2, key1}).get()); + + // second set is empty + assertEquals( + Set.of(gs("a"), gs("b"), gs("c")), client.sdiff(new GlideString[] {key1, key3}).get()); + + // first set is empty + assertEquals(Set.of(), client.sdiff(new GlideString[] {key3, key1}).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key3, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.sdiff(new GlideString[] {key1, key3}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void smismember(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + assertEquals(2, client.sadd(key1, new String[] {"one", "two"}).get()); + assertArrayEquals( + new Boolean[] {true, false}, client.smismember(key1, new String[] {"one", "three"}).get()); + + // empty set + assertArrayEquals( + new Boolean[] {false, false}, client.smismember(key2, new String[] {"one", "three"}).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.smismember(key2, new String[] {"_"}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void smismember_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + assertEquals(2, client.sadd(key1, new GlideString[] {gs("one"), gs("two")}).get()); + assertArrayEquals( + new Boolean[] {true, false}, + client.smismember(key1, new GlideString[] {gs("one"), gs("three")}).get()); + + // empty set + assertArrayEquals( + new Boolean[] {false, false}, + client.smismember(key2, new GlideString[] {gs("one"), gs("three")}).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.smismember(key2, new GlideString[] {gs("_")}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sdiffstore(BaseClient client) { + String key1 = "{key}-1-" + UUID.randomUUID(); + String key2 = "{key}-2-" + UUID.randomUUID(); + String key3 = "{key}-3-" + UUID.randomUUID(); + String key4 = "{key}-4-" + UUID.randomUUID(); + String key5 = "{key}-5-" + UUID.randomUUID(); + + assertEquals(3, client.sadd(key1, new String[] {"a", "b", "c"}).get()); + assertEquals(3, client.sadd(key2, new String[] {"c", "d", "e"}).get()); + assertEquals(3, client.sadd(key4, new String[] {"e", "f", "g"}).get()); + + // create new + assertEquals(2, client.sdiffstore(key3, new String[] {key1, key2}).get()); + assertEquals(Set.of("a", "b"), client.smembers(key3).get()); + + // overwrite existing set + assertEquals(2, client.sdiffstore(key2, new String[] {key3, key2}).get()); + assertEquals(Set.of("a", "b"), client.smembers(key2).get()); + + // overwrite source + assertEquals(3, client.sdiffstore(key1, new String[] {key1, key4}).get()); + assertEquals(Set.of("a", "b", "c"), client.smembers(key1).get()); + + // diff with empty set + assertEquals(3, client.sdiffstore(key1, new String[] {key1, key5}).get()); + assertEquals(Set.of("a", "b", "c"), client.smembers(key1).get()); + + // diff empty with non-empty set + assertEquals(0, client.sdiffstore(key3, new String[] {key5, key1}).get()); + assertEquals(Set.of(), client.smembers(key3).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key5, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.sdiffstore(key1, new String[] {key5}).get()); assertInstanceOf(RequestException.class, executionException.getCause()); // overwrite destination - not a set @@ -1113,16 +2538,59 @@ public void sdiffstore(BaseClient client) { executionException = assertThrows(ExecutionException.class, () -> client.sdiffstore(key5, new String[0]).get()); assertInstanceOf(RequestException.class, executionException.getCause()); + } - // same-slot requirement - if (client instanceof RedisClusterClient) { - executionException = - assertThrows( - ExecutionException.class, - () -> client.sdiffstore("abc", new String[] {"zxy", "lkn"}).get()); - assertInstanceOf(RequestException.class, executionException.getCause()); - assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); - } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sdiffstore_gs(BaseClient client) { + GlideString key1 = gs("{key}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3-" + UUID.randomUUID()); + GlideString key4 = gs("{key}-4-" + UUID.randomUUID()); + GlideString key5 = gs("{key}-5-" + UUID.randomUUID()); + + assertEquals(3, client.sadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(3, client.sadd(key2, new GlideString[] {gs("c"), gs("d"), gs("e")}).get()); + assertEquals(3, client.sadd(key4, new GlideString[] {gs("e"), gs("f"), gs("g")}).get()); + + // create new + assertEquals(2, client.sdiffstore(key3, new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(gs("a"), gs("b")), client.smembers(key3).get()); + + // overwrite existing set + assertEquals(2, client.sdiffstore(key2, new GlideString[] {key3, key2}).get()); + assertEquals(Set.of(gs("a"), gs("b")), client.smembers(key2).get()); + + // overwrite source + assertEquals(3, client.sdiffstore(key1, new GlideString[] {key1, key4}).get()); + assertEquals(Set.of(gs("a"), gs("b"), gs("c")), client.smembers(key1).get()); + + // diff with empty set + assertEquals(3, client.sdiffstore(key1, new GlideString[] {key1, key5}).get()); + assertEquals(Set.of(gs("a"), gs("b"), gs("c")), client.smembers(key1).get()); + + // diff empty with non-empty set + assertEquals(0, client.sdiffstore(key3, new GlideString[] {key5, key1}).get()); + assertEquals(Set.of(), client.smembers(key3).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key5, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.sdiffstore(key1, new GlideString[] {key5}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // overwrite destination - not a set + assertEquals(1, client.sdiffstore(key5, new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(gs("c")), client.smembers(key5).get()); + + // wrong arguments + executionException = + assertThrows( + ExecutionException.class, () -> client.sdiffstore(key5, new GlideString[0]).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @@ -1143,16 +2611,26 @@ public void sinter(BaseClient client) { ExecutionException executionException = assertThrows(ExecutionException.class, () -> client.sinter(new String[] {key3}).get()); assertInstanceOf(RequestException.class, executionException.getCause()); + } - // same-slot requirement - if (client instanceof RedisClusterClient) { - executionException = - assertThrows( - ExecutionException.class, - () -> client.sinter(new String[] {"abc", "zxy", "lkn"}).get()); - assertInstanceOf(RequestException.class, executionException.getCause()); - assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); - } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sinter_gs(BaseClient client) { + GlideString key1 = gs("{sinter}-" + UUID.randomUUID()); + GlideString key2 = gs("{sinter}-" + UUID.randomUUID()); + GlideString key3 = gs("{sinter}-" + UUID.randomUUID()); + + assertEquals(3, client.sadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(3, client.sadd(key2, new GlideString[] {gs("c"), gs("d"), gs("e")}).get()); + assertEquals(Set.of(gs("c")), client.sinter(new GlideString[] {key1, key2}).get()); + assertEquals(0, client.sinter(new GlideString[] {key1, key3}).get().size()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key3, gs("bar")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sinter(new GlideString[] {key3}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @@ -1196,16 +2674,54 @@ public void sunionstore(BaseClient client) { executionException = assertThrows(ExecutionException.class, () -> client.sunionstore(key5, new String[0]).get()); assertInstanceOf(RequestException.class, executionException.getCause()); + } - // same-slot requirement - if (client instanceof RedisClusterClient) { - executionException = - assertThrows( - ExecutionException.class, - () -> client.sunionstore("abc", new String[] {"zxy", "lkn"}).get()); - assertInstanceOf(RequestException.class, executionException.getCause()); - assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); - } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sunionstore_binary(BaseClient client) { + GlideString key1 = gs("{key}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3-" + UUID.randomUUID()); + GlideString key4 = gs("{key}-4-" + UUID.randomUUID()); + GlideString key5 = gs("{key}-5-" + UUID.randomUUID()); + + assertEquals(3, client.sadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(3, client.sadd(key2, new GlideString[] {gs("c"), gs("d"), gs("e")}).get()); + assertEquals(3, client.sadd(key4, new GlideString[] {gs("e"), gs("f"), gs("g")}).get()); + + // create new + assertEquals(5, client.sunionstore(key3, new GlideString[] {key1, key2}).get()); + assertEquals(Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e")), client.smembers(key3).get()); + + // overwrite existing set + assertEquals(5, client.sunionstore(key2, new GlideString[] {key3, key2}).get()); + assertEquals(Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e")), client.smembers(key2).get()); + + // overwrite source + assertEquals(6, client.sunionstore(key1, new GlideString[] {key1, key4}).get()); + assertEquals( + Set.of(gs("a"), gs("b"), gs("c"), gs("e"), gs("f"), gs("g")), client.smembers(key1).get()); + + // source key exists, but it is not a set + assertEquals(OK, client.set(key5, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.sunionstore(key1, new GlideString[] {key5}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // overwrite destination - not a set + assertEquals(7, client.sunionstore(key5, new GlideString[] {key1, key2}).get()); + assertEquals( + Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e"), gs("f"), gs("g")), + client.smembers(key5).get()); + + // wrong arguments + executionException = + assertThrows( + ExecutionException.class, () -> client.sunionstore(key5, new GlideString[0]).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @@ -1229,7 +2745,25 @@ public void exists_multiple_keys(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void expire_pexpire_and_ttl_with_positive_timeout(BaseClient client) { + public void exists_binary_multiple_keys(BaseClient client) { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + GlideString value = gs(UUID.randomUUID().toString()); + + String setResult = client.set(key1, value).get(); + assertEquals(OK, setResult); + setResult = client.set(key2, value).get(); + assertEquals(OK, setResult); + + Long existsKeysNum = + client.exists(new GlideString[] {key1, key2, key1, gs(UUID.randomUUID().toString())}).get(); + assertEquals(3L, existsKeysNum); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expire_pexpire_ttl_and_expiretime_with_positive_timeout(BaseClient client) { String key = UUID.randomUUID().toString(); assertEquals(OK, client.set(key, "expire_timeout").get()); assertTrue(client.expire(key, 10L).get()); @@ -1237,7 +2771,36 @@ public void expire_pexpire_and_ttl_with_positive_timeout(BaseClient client) { // set command clears the timeout. assertEquals(OK, client.set(key, "pexpire_timeout").get()); - if (REDIS_VERSION.isLowerThan("7.0.0")) { + if (SERVER_VERSION.isLowerThan("7.0.0")) { + assertTrue(client.pexpire(key, 10000L).get()); + } else { + assertTrue(client.pexpire(key, 10000L, ExpireOptions.HAS_NO_EXPIRY).get()); + } + assertTrue(client.ttl(key).get() <= 10L); + + // TTL will be updated to the new value = 15 + if (SERVER_VERSION.isLowerThan("7.0.0")) { + assertTrue(client.expire(key, 15L).get()); + } else { + assertTrue(client.expire(key, 15L, ExpireOptions.HAS_EXISTING_EXPIRY).get()); + assertTrue(client.expiretime(key).get() > Instant.now().getEpochSecond()); + assertTrue(client.pexpiretime(key).get() > Instant.now().toEpochMilli()); + } + assertTrue(client.ttl(key).get() <= 15L); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expire_pexpire_ttl_and_expiretime_binary_with_positive_timeout(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + assertEquals(OK, client.set(key, gs("expire_timeout")).get()); + assertTrue(client.expire(key, 10L).get()); + assertTrue(client.ttl(key).get() <= 10L); + + // set command clears the timeout. + assertEquals(OK, client.set(key, gs("pexpire_timeout")).get()); + if (SERVER_VERSION.isLowerThan("7.0.0")) { assertTrue(client.pexpire(key, 10000L).get()); } else { assertTrue(client.pexpire(key, 10000L, ExpireOptions.HAS_NO_EXPIRY).get()); @@ -1245,10 +2808,12 @@ public void expire_pexpire_and_ttl_with_positive_timeout(BaseClient client) { assertTrue(client.ttl(key).get() <= 10L); // TTL will be updated to the new value = 15 - if (REDIS_VERSION.isLowerThan("7.0.0")) { + if (SERVER_VERSION.isLowerThan("7.0.0")) { assertTrue(client.expire(key, 15L).get()); } else { assertTrue(client.expire(key, 15L, ExpireOptions.HAS_EXISTING_EXPIRY).get()); + assertTrue(client.expiretime(key).get() > Instant.now().getEpochSecond()); + assertTrue(client.pexpiretime(key).get() > Instant.now().toEpochMilli()); } assertTrue(client.ttl(key).get() <= 15L); } @@ -1263,7 +2828,7 @@ public void expireAt_pexpireAt_and_ttl_with_positive_timeout(BaseClient client) assertTrue(client.ttl(key).get() <= 10L); // extend TTL - if (REDIS_VERSION.isLowerThan("7.0.0")) { + if (SERVER_VERSION.isLowerThan("7.0.0")) { assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 50L).get()); } else { assertTrue( @@ -1276,7 +2841,7 @@ public void expireAt_pexpireAt_and_ttl_with_positive_timeout(BaseClient client) } assertTrue(client.ttl(key).get() <= 50L); - if (REDIS_VERSION.isLowerThan("7.0.0")) { + if (SERVER_VERSION.isLowerThan("7.0.0")) { assertTrue(client.pexpireAt(key, Instant.now().toEpochMilli() + 50000L).get()); } else { // set command clears the timeout. @@ -1292,11 +2857,54 @@ public void expireAt_pexpireAt_and_ttl_with_positive_timeout(BaseClient client) @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void expire_pexpire_ttl_with_timestamp_in_the_past_or_negative_timeout(BaseClient client) { + public void expireAt_pexpireAt_and_ttl_binary_with_positive_timeout(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + assertEquals(OK, client.set(key, gs("expireAt_timeout")).get()); + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 10L).get()); + assertTrue(client.ttl(key).get() <= 10L); + + // extend TTL + if (SERVER_VERSION.isLowerThan("7.0.0")) { + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 50L).get()); + } else { + assertTrue( + client + .expireAt( + key, + Instant.now().getEpochSecond() + 50L, + ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) + .get()); + } + assertTrue(client.ttl(key).get() <= 50L); + + if (SERVER_VERSION.isLowerThan("7.0.0")) { + assertTrue(client.pexpireAt(key, Instant.now().toEpochMilli() + 50000L).get()); + } else { + // set command clears the timeout. + assertEquals(OK, client.set(key, gs("pexpireAt_timeout")).get()); + assertFalse( + client + .pexpireAt( + key, Instant.now().toEpochMilli() + 50000L, ExpireOptions.HAS_EXISTING_EXPIRY) + .get()); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expire_pexpire_ttl_and_expiretime_with_timestamp_in_the_past_or_negative_timeout( + BaseClient client) { String key = UUID.randomUUID().toString(); assertEquals(OK, client.set(key, "expire_with_past_timestamp").get()); + // no timeout set yet assertEquals(-1L, client.ttl(key).get()); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(-1L, client.expiretime(key).get()); + assertEquals(-1L, client.pexpiretime(key).get()); + } + assertTrue(client.expire(key, -10L).get()); assertEquals(-2L, client.ttl(key).get()); @@ -1305,6 +2913,30 @@ public void expire_pexpire_ttl_with_timestamp_in_the_past_or_negative_timeout(Ba assertEquals(-2L, client.ttl(key).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void + expire_pexpire_ttl_and_expiretime_binary_with_timestamp_in_the_past_or_negative_timeout( + BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("expire_with_past_timestamp")).get()); + // no timeout set yet + assertEquals(-1L, client.ttl(key).get()); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(-1L, client.expiretime(key).get()); + assertEquals(-1L, client.pexpiretime(key).get()); + } + + assertTrue(client.expire(key, -10L).get()); + assertEquals(-2L, client.ttl(key).get()); + + assertEquals(OK, client.set(key, gs("pexpire_with_past_timestamp")).get()); + assertTrue(client.pexpire(key, -10000L).get()); + assertEquals(-2L, client.ttl(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1326,13 +2958,51 @@ public void expireAt_pexpireAt_ttl_with_timestamp_in_the_past_or_negative_timeou @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void expire_pexpire_and_ttl_with_non_existing_key(BaseClient client) { + public void expireAt_pexpireAt_ttl_binary_with_timestamp_in_the_past_or_negative_timeout( + BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(OK, client.set(key, gs("expireAt_with_past_timestamp")).get()); + // set timeout in the past + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() - 50L).get()); + assertEquals(-2L, client.ttl(key).get()); + + assertEquals(OK, client.set(key, gs("pexpireAt_with_past_timestamp")).get()); + // set timeout in the past + assertTrue(client.pexpireAt(key, Instant.now().toEpochMilli() - 50000L).get()); + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expire_pexpire_ttl_and_expiretime_with_non_existing_key(BaseClient client) { String key = UUID.randomUUID().toString(); assertFalse(client.expire(key, 10L).get()); assertFalse(client.pexpire(key, 10000L).get()); assertEquals(-2L, client.ttl(key).get()); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(-2L, client.expiretime(key).get()); + assertEquals(-2L, client.pexpiretime(key).get()); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expire_pexpire_ttl_and_expiretime_binary_with_non_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertFalse(client.expire(key, 10L).get()); + assertFalse(client.pexpire(key, 10000L).get()); + + assertEquals(-2L, client.ttl(key).get()); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(-2L, client.expiretime(key).get()); + assertEquals(-2L, client.pexpiretime(key).get()); + } } @SneakyThrows @@ -1347,6 +3017,18 @@ public void expireAt_pexpireAt_and_ttl_with_non_existing_key(BaseClient client) assertEquals(-2L, client.ttl(key).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expireAt_pexpireAt_and_ttl_binary_with_non_existing_key(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertFalse(client.expireAt(key, Instant.now().getEpochSecond() + 10L).get()); + assertFalse(client.pexpireAt(key, Instant.now().toEpochMilli() + 10000L).get()); + + assertEquals(-2L, client.ttl(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1370,6 +3052,29 @@ public void expire_pexpire_and_pttl_with_positive_timeout(BaseClient client) { assertTrue(pttlResult <= 10000L); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void expire_pexpire_and_pttl_binary_with_positive_timeout(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + assertEquals(-2L, client.pttl(key).get()); + + assertEquals(OK, client.set(key, gs("expire_timeout")).get()); + assertTrue(client.expire(key, 10L).get()); + Long pttlResult = client.pttl(key).get(); + assertTrue(0 <= pttlResult); + assertTrue(pttlResult <= 10000L); + + assertEquals(OK, client.set(key, gs("pexpire_timeout")).get()); + assertEquals(-1L, client.pttl(key).get()); + + assertTrue(client.pexpire(key, 10000L).get()); + pttlResult = client.pttl(key).get(); + assertTrue(0 <= pttlResult); + assertTrue(pttlResult <= 10000L); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1378,14 +3083,13 @@ public void persist_on_existing_and_non_existing_key(BaseClient client) { assertFalse(client.persist(key).get()); - assertEquals(OK, client.set(key, "persist_value").get()); - assertFalse(client.persist(key).get()); + assertEquals(OK, client.set(gs(key), gs("persist_value")).get()); + assertFalse(client.persist(gs(key)).get()); assertTrue(client.expire(key, 10L).get()); Long persistAmount = client.ttl(key).get(); assertTrue(0L <= persistAmount && persistAmount <= 10L); - assertTrue(client.persist(key).get()); - + assertTrue(client.persist(gs(key)).get()); assertEquals(-1L, client.ttl(key).get()); } @@ -1396,12 +3100,12 @@ public void invokeScript_test(BaseClient client) { String key1 = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); - try (Script script = new Script("return 'Hello'")) { + try (Script script = new Script("return 'Hello'", false)) { Object response = client.invokeScript(script).get(); assertEquals("Hello", response); } - try (Script script = new Script("return redis.call('SET', KEYS[1], ARGV[1])")) { + try (Script script = new Script("return redis.call('SET', KEYS[1], ARGV[1])", false)) { Object setResponse1 = client .invokeScript(script, ScriptOptions.builder().key(key1).arg("value1").build()) @@ -1415,17 +3119,96 @@ public void invokeScript_test(BaseClient client) { assertEquals(OK, setResponse2); } - try (Script script = new Script("return redis.call('GET', KEYS[1])")) { + try (Script script = new Script("return redis.call('GET', KEYS[1])", false)) { Object getResponse1 = client.invokeScript(script, ScriptOptions.builder().key(key1).build()).get(); assertEquals("value1", getResponse1); + // Use GlideString in option but we still expect nonbinary output Object getResponse2 = - client.invokeScript(script, ScriptOptions.builder().key(key2).build()).get(); + client + .invokeScript(script, ScriptOptionsGlideString.builder().key(gs(key2)).build()) + .get(); assertEquals("value2", getResponse2); } } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void script_large_keys_and_or_args(BaseClient client) { + String str1 = "0".repeat(1 << 12); // 4k + String str2 = "0".repeat(1 << 12); // 4k + + try (Script script = new Script("return KEYS[1]", false)) { + // 1 very big key + Object response = + client.invokeScript(script, ScriptOptions.builder().key(str1 + str2).build()).get(); + assertEquals(str1 + str2, response); + } + + try (Script script = new Script("return KEYS[1]", false)) { + // 2 big keys + Object response = + client.invokeScript(script, ScriptOptions.builder().key(str1).key(str2).build()).get(); + assertEquals(str1, response); + } + + try (Script script = new Script("return ARGV[1]", false)) { + // 1 very big arg + Object response = + client.invokeScript(script, ScriptOptions.builder().arg(str1 + str2).build()).get(); + assertEquals(str1 + str2, response); + } + + try (Script script = new Script("return ARGV[1]", false)) { + // 1 big arg + 1 big key + Object response = + client.invokeScript(script, ScriptOptions.builder().arg(str1).key(str2).build()).get(); + assertEquals(str2, response); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void invokeScript_gs_test(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + try (Script script = new Script(gs("return 'Hello'"), true)) { + Object response = client.invokeScript(script).get(); + assertEquals(gs("Hello"), response); + } + + try (Script script = new Script(gs("return redis.call('SET', KEYS[1], ARGV[1])"), true)) { + Object setResponse1 = + client + .invokeScript( + script, ScriptOptionsGlideString.builder().key(key1).arg(gs("value1")).build()) + .get(); + assertEquals(OK, setResponse1); + + Object setResponse2 = + client + .invokeScript( + script, ScriptOptionsGlideString.builder().key(key2).arg(gs("value2")).build()) + .get(); + assertEquals(OK, setResponse2); + } + + try (Script script = new Script(gs("return redis.call('GET', KEYS[1])"), true)) { + Object getResponse1 = + client.invokeScript(script, ScriptOptionsGlideString.builder().key(key1).build()).get(); + assertEquals(gs("value1"), getResponse1); + + // Use String in option but we still expect binary output (GlideString) + Object getResponse2 = + client.invokeScript(script, ScriptOptions.builder().key(key2.toString()).build()).get(); + assertEquals(gs("value2"), getResponse2); + } + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1437,6 +3220,18 @@ public void zadd_and_zaddIncr(BaseClient client) { assertEquals(3.0, client.zaddIncr(key, "one", 2.0).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zadd_binary_and_zaddIncr_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + + assertEquals(3, client.zadd(key, membersScores).get()); + assertEquals(3.0, client.zaddIncr(key, gs("one"), 2.0).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1453,6 +3248,24 @@ public void zadd_and_zaddIncr_wrong_type(BaseClient client) { assertTrue(executionExceptionZaddIncr.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zadd_binary_and_zaddIncr_binary_wrong_type(BaseClient client) { + assertEquals(OK, client.set(gs("foo"), gs("bar")).get()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + + ExecutionException executionExceptionZadd = + assertThrows(ExecutionException.class, () -> client.zadd(gs("foo"), membersScores).get()); + assertTrue(executionExceptionZadd.getCause() instanceof RequestException); + + ExecutionException executionExceptionZaddIncr = + assertThrows( + ExecutionException.class, () -> client.zaddIncr(gs("foo"), gs("one"), 2.0).get()); + assertTrue(executionExceptionZaddIncr.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1460,13 +3273,13 @@ public void zadd_and_zaddIncr_with_NX_XX(BaseClient client) { String key = UUID.randomUUID().toString(); Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); - ZaddOptions onlyIfExistsOptions = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_EXISTS) + ZAddOptions onlyIfExistsOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) .build(); - ZaddOptions onlyIfDoesNotExistOptions = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + ZAddOptions onlyIfDoesNotExistOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) .build(); assertEquals(0, client.zadd(key, membersScores, onlyIfExistsOptions).get()); @@ -1475,6 +3288,29 @@ public void zadd_and_zaddIncr_with_NX_XX(BaseClient client) { assertEquals(6, client.zaddIncr(key, "one", 5, onlyIfExistsOptions).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zadd_binary_and_zaddIncr_binary_with_NX_XX(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + + ZAddOptions onlyIfExistsOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .build(); + ZAddOptions onlyIfDoesNotExistOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .build(); + + assertEquals(0, client.zadd(key, membersScores, onlyIfExistsOptions).get()); + assertEquals(3, client.zadd(key, membersScores, onlyIfDoesNotExistOptions).get()); + assertNull(client.zaddIncr(key, gs("one"), 5, onlyIfDoesNotExistOptions).get()); + assertEquals(6, client.zaddIncr(key, gs("one"), 5, onlyIfExistsOptions).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1488,13 +3324,13 @@ public void zadd_and_zaddIncr_with_GT_LT(BaseClient client) { assertEquals(3, client.zadd(key, membersScores).get()); membersScores.put("one", 10.0); - ZaddOptions scoreGreaterThanOptions = - ZaddOptions.builder() - .updateOptions(ZaddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + ZAddOptions scoreGreaterThanOptions = + ZAddOptions.builder() + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) .build(); - ZaddOptions scoreLessThanOptions = - ZaddOptions.builder() - .updateOptions(ZaddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + ZAddOptions scoreLessThanOptions = + ZAddOptions.builder() + .updateOptions(ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) .build(); assertEquals(1, client.zadd(key, membersScores, scoreGreaterThanOptions, true).get()); @@ -1503,38 +3339,100 @@ public void zadd_and_zaddIncr_with_GT_LT(BaseClient client) { assertNull(client.zaddIncr(key, "one", -3, scoreGreaterThanOptions).get()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zadd_binary_and_zaddIncr_binary_with_GT_LT(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = new LinkedHashMap<>(); + membersScores.put(gs("one"), -3.0); + membersScores.put(gs("two"), 2.0); + membersScores.put(gs("three"), 3.0); + + assertEquals(3, client.zadd(key, membersScores).get()); + membersScores.put(gs("one"), 10.0); + + ZAddOptions scoreGreaterThanOptions = + ZAddOptions.builder() + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + ZAddOptions scoreLessThanOptions = + ZAddOptions.builder() + .updateOptions(ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + .build(); + + assertEquals(1, client.zadd(key, membersScores, scoreGreaterThanOptions, true).get()); + assertEquals(0, client.zadd(key, membersScores, scoreLessThanOptions, true).get()); + assertEquals(7, client.zaddIncr(key, gs("one"), -3, scoreLessThanOptions).get()); + assertNull(client.zaddIncr(key, gs("one"), -3, scoreGreaterThanOptions).get()); + } + // TODO move to another class @Test public void zadd_illegal_arguments() { - ZaddOptions existsGreaterThanOptions = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) - .updateOptions(ZaddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + ZAddOptions existsGreaterThanOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) .build(); assertThrows(IllegalArgumentException.class, existsGreaterThanOptions::toArgs); - ZaddOptions existsLessThanOptions = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) - .updateOptions(ZaddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + ZAddOptions existsLessThanOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) .build(); assertThrows(IllegalArgumentException.class, existsLessThanOptions::toArgs); - ZaddOptions options = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + ZAddOptions options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .build(); + options.toArgs(); + options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) .build(); options.toArgs(); options = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_EXISTS) - .updateOptions(ZaddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + .build(); + options.toArgs(); + } + + // TODO move to another class + @Test + public void zadd_binary_illegal_arguments() { + ZAddOptions existsGreaterThanOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) + .build(); + assertThrows(IllegalArgumentException.class, existsGreaterThanOptions::toArgs); + ZAddOptions existsLessThanOptions = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + .build(); + assertThrows(IllegalArgumentException.class, existsLessThanOptions::toArgs); + ZAddOptions options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_DOES_NOT_EXIST) .build(); options.toArgs(); options = - ZaddOptions.builder() - .conditionalChange(ZaddOptions.ConditionalChange.ONLY_IF_EXISTS) - .updateOptions(ZaddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_GREATER_THAN_CURRENT) .build(); options.toArgs(); + options = + ZAddOptions.builder() + .conditionalChange(ZAddOptions.ConditionalChange.ONLY_IF_EXISTS) + .updateOptions(ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT) + .build(); + options.toArgsBinary(); } @SneakyThrows @@ -1598,78 +3496,464 @@ public void zpopmin(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zpopmax(BaseClient client) { - String key = UUID.randomUUID().toString(); + public void zpopmin_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); - assertEquals(3, client.zadd(key, membersScores).get()); - assertEquals(Map.of("c", 3.0), client.zpopmax(key).get()); - assertEquals(Map.of("b", 2.0, "a", 1.0), client.zpopmax(key, 3).get()); - assertTrue(client.zpopmax(key).get().isEmpty()); - assertTrue(client.zpopmax("non_existing_key").get().isEmpty()); + assertEquals( + 3, + client + .zadd(key.toString(), membersScores) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals(Map.of(gs("a"), 1.0), client.zpopmin(key).get()); + assertEquals(Map.of(gs("b"), 2.0, gs("c"), 3.0), client.zpopmin(key, 3).get()); + assertTrue(client.zpopmin(key).get().isEmpty()); + assertTrue(client.zpopmin(gs("non_existing_key")).get().isEmpty()); // Key exists, but it is not a set - assertEquals(OK, client.set(key, "value").get()); + assertEquals(OK, client.set(key, gs("value")).get()); ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.zpopmax(key).get()); + assertThrows(ExecutionException.class, () -> client.zpopmin(key).get()); assertTrue(executionException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zscore(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); + public void bzpopmin(BaseClient client) { + String key1 = "{test}-1-" + UUID.randomUUID(); + String key2 = "{test}-2-" + UUID.randomUUID(); + String key3 = "{test}-3-" + UUID.randomUUID(); - Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); - assertEquals(3, client.zadd(key1, membersScores).get()); - assertEquals(1.0, client.zscore(key1, "one").get()); - assertNull(client.zscore(key1, "non_existing_member").get()); - assertNull(client.zscore("non_existing_key", "non_existing_member").get()); + assertEquals(2, client.zadd(key1, Map.of("a", 1.0, "b", 1.5)).get()); + assertEquals(1, client.zadd(key2, Map.of("c", 2.0)).get()); + assertArrayEquals( + new Object[] {key1, "a", 1.0}, client.bzpopmin(new String[] {key1, key2}, .5).get()); - // Key exists, but it is not a set - assertEquals(OK, client.set(key2, "bar").get()); + // nothing popped out - key does not exist + assertNull( + client + .bzpopmin(new String[] {key3}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // pops from the second key + assertArrayEquals( + new Object[] {key2, "c", 2.0}, client.bzpopmin(new String[] {key3, key2}, .5).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, "value").get()); ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.zscore(key2, "one").get()); - assertTrue(executionException.getCause() instanceof RequestException); + assertThrows( + ExecutionException.class, () -> client.bzpopmin(new String[] {key3}, .5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zrank(BaseClient client) { + public void bzpopmin_binary(BaseClient client) { + GlideString key1 = gs("{test}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{test}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{test}-3-" + UUID.randomUUID()); + + assertEquals( + 2, + client + .zadd(key1.toString(), Map.of("a", 1.0, "b", 1.5)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals( + 1, + client + .zadd(key2.toString(), Map.of("c", 2.0)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertArrayEquals( + new Object[] {key1, gs("a"), 1.0}, + client.bzpopmin(new GlideString[] {key1, key2}, .5).get()); + + // nothing popped out - key does not exist + assertNull( + client + .bzpopmin(new GlideString[] {key3}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // pops from the second key + assertArrayEquals( + new Object[] {key2, gs("c"), 2.0}, + client.bzpopmin(new GlideString[] {key3, key2}, .5).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.bzpopmin(new GlideString[] {key3}, .5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzpopmin_timeout_check(BaseClient client) { String key = UUID.randomUUID().toString(); - Map membersScores = Map.of("one", 1.5, "two", 2.0, "three", 3.0); - assertEquals(3, client.zadd(key, membersScores).get()); - assertEquals(0, client.zrank(key, "one").get()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.bzpopmin(new String[] {key}, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.bzpopmin(new String[] {key}, 0).get(3, TimeUnit.SECONDS)); + } + } - if (REDIS_VERSION.isGreaterThanOrEqualTo("7.2.0")) { - assertArrayEquals(new Object[] {0L, 1.5}, client.zrankWithScore(key, "one").get()); - assertNull(client.zrankWithScore(key, "nonExistingMember").get()); - assertNull(client.zrankWithScore("nonExistingKey", "nonExistingMember").get()); + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzpopmin_binary_timeout_check(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.bzpopmin(new GlideString[] {key}, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.bzpopmin(new GlideString[] {key}, 0).get(3, TimeUnit.SECONDS)); } - assertNull(client.zrank(key, "nonExistingMember").get()); - assertNull(client.zrank("nonExistingKey", "nonExistingMember").get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zpopmax(BaseClient client) { + String key = UUID.randomUUID().toString(); + Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + assertEquals(Map.of("c", 3.0), client.zpopmax(key).get()); + assertEquals(Map.of("b", 2.0, "a", 1.0), client.zpopmax(key, 3).get()); + assertTrue(client.zpopmax(key).get().isEmpty()); + assertTrue(client.zpopmax("non_existing_key").get().isEmpty()); // Key exists, but it is not a set assertEquals(OK, client.set(key, "value").get()); ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.zrank(key, "one").get()); + assertThrows(ExecutionException.class, () -> client.zpopmax(key).get()); assertTrue(executionException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zdiff(BaseClient client) { - String key1 = "{testKey}:1-" + UUID.randomUUID(); - String key2 = "{testKey}:2-" + UUID.randomUUID(); - String key3 = "{testKey}:3-" + UUID.randomUUID(); - String nonExistentKey = "{testKey}:4-" + UUID.randomUUID(); + public void zpopmax_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); + assertEquals( + 3, + client + .zadd(key.toString(), membersScores) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals(Map.of(gs("c"), 3.0), client.zpopmax(key).get()); + assertEquals(Map.of(gs("b"), 2.0, gs("a"), 1.0), client.zpopmax(key, 3).get()); + assertTrue(client.zpopmax(key).get().isEmpty()); + assertTrue(client.zpopmax(gs("non_existing_key")).get().isEmpty()); - Map membersScores1 = Map.of("one", 1.0, "two", 2.0, "three", 3.0); - Map membersScores2 = Map.of("two", 2.0); - Map membersScores3 = Map.of("one", 0.5, "two", 2.0, "three", 3.0, "four", 4.0); + // Key exists, but it is not a set + assertEquals(OK, client.set(key, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zpopmax(key).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzpopmax(BaseClient client) { + String key1 = "{test}-1-" + UUID.randomUUID(); + String key2 = "{test}-2-" + UUID.randomUUID(); + String key3 = "{test}-3-" + UUID.randomUUID(); + + assertEquals(2, client.zadd(key1, Map.of("a", 1.0, "b", 1.5)).get()); + assertEquals(1, client.zadd(key2, Map.of("c", 2.0)).get()); + assertArrayEquals( + new Object[] {key1, "b", 1.5}, client.bzpopmax(new String[] {key1, key2}, .5).get()); + + // nothing popped out - key does not exist + assertNull( + client + .bzpopmax(new String[] {key3}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // pops from the second key + assertArrayEquals( + new Object[] {key2, "c", 2.0}, client.bzpopmax(new String[] {key3, key2}, .5).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.bzpopmax(new String[] {key3}, .5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzpopmax_binary(BaseClient client) { + GlideString key1 = gs("{test}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{test}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{test}-3-" + UUID.randomUUID()); + + assertEquals( + 2, + client + .zadd(key1.toString(), Map.of("a", 1.0, "b", 1.5)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals( + 1, + client + .zadd(key2.toString(), Map.of("c", 2.0)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertArrayEquals( + new Object[] {key1, gs("b"), 1.5}, + client.bzpopmax(new GlideString[] {key1, key2}, .5).get()); + + // nothing popped out - key does not exist + assertNull( + client + .bzpopmax(new GlideString[] {key3}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // pops from the second key + assertArrayEquals( + new Object[] {key2, gs("c"), 2.0}, + client.bzpopmax(new GlideString[] {key3, key2}, .5).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.bzpopmax(new GlideString[] {key3}, .5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzpopmax_timeout_check(BaseClient client) { + String key = UUID.randomUUID().toString(); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.bzpopmax(new String[] {key}, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.bzpopmax(new String[] {key}, 0).get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzpopmax_binary_timeout_check(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.bzpopmax(new GlideString[] {key}, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.bzpopmax(new GlideString[] {key}, 0).get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zscore(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); + assertEquals(3, client.zadd(key1, membersScores).get()); + assertEquals(1.0, client.zscore(key1, "one").get()); + assertNull(client.zscore(key1, "non_existing_member").get()); + assertNull(client.zscore("non_existing_key", "non_existing_member").get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zscore(key2, "one").get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrevrank_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = + Map.of(gs("one"), 1.5, gs("two"), 2.0, gs("three"), 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + assertEquals(0, client.zrevrank(key, gs("three")).get()); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")) { + assertArrayEquals(new Object[] {2L, 1.5}, client.zrevrankWithScore(key, gs("one")).get()); + assertNull(client.zrevrankWithScore(key, gs("nonExistingMember")).get()); + assertNull(client.zrevrankWithScore(gs("nonExistingKey"), gs("nonExistingMember")).get()); + } + assertNull(client.zrevrank(key, gs("nonExistingMember")).get()); + assertNull(client.zrevrank(gs("nonExistingKey"), gs("nonExistingMember")).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrevrank(key, gs("one")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrank(BaseClient client) { + String key = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.5, "two", 2.0, "three", 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + assertEquals(0, client.zrank(key, "one").get()); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")) { + assertArrayEquals(new Object[] {0L, 1.5}, client.zrankWithScore(key, "one").get()); + assertNull(client.zrankWithScore(key, "nonExistingMember").get()); + assertNull(client.zrankWithScore("nonExistingKey", "nonExistingMember").get()); + } + assertNull(client.zrank(key, "nonExistingMember").get()); + assertNull(client.zrank("nonExistingKey", "nonExistingMember").get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, "value").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrank(key, "one").get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zdiff_binary(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in 6.2.0"); + + GlideString key1 = gs("{testKey}:1-" + UUID.randomUUID()); + GlideString key2 = gs("{testKey}:2-" + UUID.randomUUID()); + GlideString key3 = gs("{testKey}:3-" + UUID.randomUUID()); + GlideString nonExistentKey = gs("{testKey}:4-" + UUID.randomUUID()); + + Map membersScores1 = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + Map membersScores2 = Map.of(gs("two"), 2.0); + Map membersScores3 = + Map.of(gs("one"), 0.5, gs("two"), 2.0, gs("three"), 3.0, gs("four"), 4.0); + + assertEquals(3, client.zadd(key1, membersScores1).get()); + assertEquals(1, client.zadd(key2, membersScores2).get()); + assertEquals(4, client.zadd(key3, membersScores3).get()); + + assertArrayEquals( + new GlideString[] {gs("one"), gs("three")}, + client.zdiff(new GlideString[] {key1, key2}).get()); + assertArrayEquals(new GlideString[] {}, client.zdiff(new GlideString[] {key1, key3}).get()); + assertArrayEquals( + new GlideString[] {}, client.zdiff(new GlideString[] {nonExistentKey, key3}).get()); + + assertEquals( + Map.of(gs("one"), 1.0, gs("three"), 3.0), + client.zdiffWithScores(new GlideString[] {key1, key2}).get()); + assertEquals(Map.of(), client.zdiffWithScores(new GlideString[] {key1, key3}).get()); + assertTrue(client.zdiffWithScores(new GlideString[] {nonExistentKey, key3}).get().isEmpty()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(nonExistentKey, gs("bar")).get()); + + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zdiff(new GlideString[] {nonExistentKey, key2}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.zdiffWithScores(new GlideString[] {nonExistentKey, key2}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrevrank(BaseClient client) { + String key = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.5, "two", 2.0, "three", 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + assertEquals(0, client.zrevrank(key, "three").get()); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")) { + assertArrayEquals(new Object[] {2L, 1.5}, client.zrevrankWithScore(key, "one").get()); + assertNull(client.zrevrankWithScore(key, "nonExistingMember").get()); + assertNull(client.zrevrankWithScore("nonExistingKey", "nonExistingMember").get()); + } + assertNull(client.zrevrank(key, "nonExistingMember").get()); + assertNull(client.zrevrank("nonExistingKey", "nonExistingMember").get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, "value").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrevrank(key, "one").get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zdiff(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + + String key1 = "{testKey}:1-" + UUID.randomUUID(); + String key2 = "{testKey}:2-" + UUID.randomUUID(); + String key3 = "{testKey}:3-" + UUID.randomUUID(); + String nonExistentKey = "{testKey}:4-" + UUID.randomUUID(); + + Map membersScores1 = Map.of("one", 1.0, "two", 2.0, "three", 3.0); + Map membersScores2 = Map.of("two", 2.0); + Map membersScores3 = Map.of("one", 0.5, "two", 2.0, "three", 3.0, "four", 4.0); assertEquals(3, client.zadd(key1, membersScores1).get()); assertEquals(1, client.zadd(key2, membersScores2).get()); @@ -1714,10 +3998,15 @@ public void zmscore(BaseClient client) { assertArrayEquals( new Double[] {1.0, null, null, 3.0}, client - .zmscore(key1, new String[] {"one", "nonExistentMember", "nonExistentMember", "three"}) + .zmscore( + gs(key1), + new GlideString[] { + gs("one"), gs("nonExistentMember"), gs("nonExistentMember"), gs("three") + }) .get()); assertArrayEquals( - new Double[] {null}, client.zmscore("nonExistentKey", new String[] {"one"}).get()); + new Double[] {null}, + client.zmscore(gs("nonExistentKey"), new GlideString[] {gs("one")}).get()); // Key exists, but it is not a set assertEquals(OK, client.set(key2, "bar").get()); @@ -1731,6 +4020,9 @@ public void zmscore(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void zdiffstore(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + String key1 = "{testKey}:1-" + UUID.randomUUID(); String key2 = "{testKey}:2-" + UUID.randomUUID(); String key3 = "{testKey}:3-" + UUID.randomUUID(); @@ -1769,6 +4061,61 @@ public void zdiffstore(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zcount_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + assertEquals(3, client.zadd(key1, membersScores).get()); + + // In range negative to positive infinity. + assertEquals( + 3, + client + .zcount(key1, InfScoreBound.NEGATIVE_INFINITY, InfScoreBound.POSITIVE_INFINITY) + .get()); + assertEquals( + 3, + client + .zcount( + key1, + new ScoreBoundary(Double.NEGATIVE_INFINITY), + new ScoreBoundary(Double.POSITIVE_INFINITY)) + .get()); + // In range 1 (exclusive) to 3 (inclusive) + assertEquals( + 2, client.zcount(key1, new ScoreBoundary(1, false), new ScoreBoundary(3, true)).get()); + // In range negative infinity to 3 (inclusive) + assertEquals( + 3, client.zcount(key1, InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, true)).get()); + // Incorrect range start > end + assertEquals( + 0, client.zcount(key1, InfScoreBound.POSITIVE_INFINITY, new ScoreBoundary(3, true)).get()); + // Non-existing key + assertEquals( + 0, + client + .zcount( + gs("non_existing_key"), + InfScoreBound.NEGATIVE_INFINITY, + InfScoreBound.POSITIVE_INFINITY) + .get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zcount(key2, InfScoreBound.NEGATIVE_INFINITY, InfScoreBound.POSITIVE_INFINITY) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1807,6 +4154,46 @@ public void zcount(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zremrangebylex_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + RangeByIndex query = new RangeByIndex(0, -1); + Map membersScores = + Map.of(gs("a"), 1.0, gs("b"), 2.0, gs("c"), 3.0, gs("d"), 4.0); + assertEquals(4, client.zadd(key1, membersScores).get()); + + assertEquals( + 2, client.zremrangebylex(key1, new LexBoundary("a", false), new LexBoundary("c")).get()); + assertEquals(Map.of(gs("a"), 1.0, gs("d"), 4.0), client.zrangeWithScores(key1, query).get()); + + assertEquals( + 1, client.zremrangebylex(key1, new LexBoundary("d"), InfLexBound.POSITIVE_INFINITY).get()); + assertEquals(Map.of(gs("a"), 1.0), client.zrangeWithScores(key1, query).get()); + + // MinLex > MaxLex + assertEquals( + 0, client.zremrangebylex(key1, new LexBoundary("a"), InfLexBound.NEGATIVE_INFINITY).get()); + assertEquals(Map.of(gs("a"), 1.0), client.zrangeWithScores(key1, query).get()); + + // Non Existing Key + assertEquals( + 0, + client + .zremrangebylex(key2, InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY) + .get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zremrangebylex(key2, new LexBoundary("a"), new LexBoundary("c")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1880,11 +4267,12 @@ public void zremrangebylex(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zremrangebyscore(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); + public void zremrangebyscore_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); RangeByIndex query = new RangeByIndex(0, -1); - Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0, "four", 4.0); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0, gs("four"), 4.0); assertEquals(4, client.zadd(key1, membersScores).get()); // MinScore > MaxScore @@ -1892,17 +4280,18 @@ public void zremrangebyscore(BaseClient client) { 0, client.zremrangebyscore(key1, new ScoreBoundary(1), InfScoreBound.NEGATIVE_INFINITY).get()); assertEquals( - Map.of("one", 1.0, "two", 2.0, "three", 3.0, "four", 4.0), + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0, gs("four"), 4.0), client.zrangeWithScores(key1, query).get()); assertEquals( 2, client.zremrangebyscore(key1, new ScoreBoundary(1, false), new ScoreBoundary(3)).get()); - assertEquals(Map.of("one", 1.0, "four", 4.0), client.zrangeWithScores(key1, query).get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("four"), 4.0), client.zrangeWithScores(key1, query).get()); assertEquals( 1, client.zremrangebyscore(key1, new ScoreBoundary(4), InfScoreBound.POSITIVE_INFINITY).get()); - assertEquals(Map.of("one", 1.0), client.zrangeWithScores(key1, query).get()); + assertEquals(Map.of(gs("one"), 1.0), client.zrangeWithScores(key1, query).get()); // Non Existing Key assertEquals( @@ -1913,7 +4302,7 @@ public void zremrangebyscore(BaseClient client) { .get()); // Key exists, but it is not a set - assertEquals(OK, client.set(key2, "value").get()); + assertEquals(OK, client.set(key2, gs("value")).get()); ExecutionException executionException = assertThrows( ExecutionException.class, @@ -1924,13 +4313,105 @@ public void zremrangebyscore(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zlexcount(BaseClient client) { + public void zremrangebyscore(BaseClient client) { String key1 = UUID.randomUUID().toString(); String key2 = UUID.randomUUID().toString(); - Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); - assertEquals(3, client.zadd(key1, membersScores).get()); + RangeByIndex query = new RangeByIndex(0, -1); + Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0, "four", 4.0); + assertEquals(4, client.zadd(key1, membersScores).get()); - // In range negative to positive infinity. + // MinScore > MaxScore + assertEquals( + 0, + client.zremrangebyscore(key1, new ScoreBoundary(1), InfScoreBound.NEGATIVE_INFINITY).get()); + assertEquals( + Map.of("one", 1.0, "two", 2.0, "three", 3.0, "four", 4.0), + client.zrangeWithScores(key1, query).get()); + + assertEquals( + 2, client.zremrangebyscore(key1, new ScoreBoundary(1, false), new ScoreBoundary(3)).get()); + assertEquals(Map.of("one", 1.0, "four", 4.0), client.zrangeWithScores(key1, query).get()); + + assertEquals( + 1, + client.zremrangebyscore(key1, new ScoreBoundary(4), InfScoreBound.POSITIVE_INFINITY).get()); + assertEquals(Map.of("one", 1.0), client.zrangeWithScores(key1, query).get()); + + // Non Existing Key + assertEquals( + 0, + client + .zremrangebyscore( + key2, InfScoreBound.NEGATIVE_INFINITY, InfScoreBound.POSITIVE_INFINITY) + .get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zremrangebyscore(key2, new ScoreBoundary(1), new ScoreBoundary(2)).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zlexcount_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + Map membersScores = Map.of(gs("a"), 1.0, gs("b"), 2.0, gs("c"), 3.0); + assertEquals(3, client.zadd(key1, membersScores).get()); + + // In range negative to positive infinity. + assertEquals( + 3, + client.zlexcount(key1, InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY).get()); + + // In range a (exclusive) to c (inclusive) + assertEquals( + 2, client.zlexcount(key1, new LexBoundary("a", false), new LexBoundary("c", true)).get()); + + // In range negative infinity to c (inclusive) + assertEquals( + 3, client.zlexcount(key1, InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", true)).get()); + + // Incorrect range start > end + assertEquals( + 0, client.zlexcount(key1, InfLexBound.POSITIVE_INFINITY, new LexBoundary("c", true)).get()); + + // Non-existing key + assertEquals( + 0, + client + .zlexcount( + gs("non_existing_key"), + InfLexBound.NEGATIVE_INFINITY, + InfLexBound.POSITIVE_INFINITY) + .get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zlexcount(key2, InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zlexcount(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); + assertEquals(3, client.zadd(key1, membersScores).get()); + + // In range negative to positive infinity. assertEquals( 3, client.zlexcount(key1, InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY).get()); @@ -1967,6 +4448,46 @@ public void zlexcount(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrangestore_binary_by_index(BaseClient client) { + GlideString key = gs("{testKey}:" + UUID.randomUUID()); + GlideString destination = gs("{testKey}:" + UUID.randomUUID()); + GlideString source = gs("{testKey}:" + UUID.randomUUID()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + assertEquals(3, client.zadd(source, membersScores).get()); + + // Full range. + assertEquals(3, client.zrangestore(destination, source, new RangeByIndex(0, -1)).get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from rank 0 to 1. In descending order of scores. + assertEquals(2, client.zrangestore(destination, source, new RangeByIndex(0, 1), true).get()); + assertEquals( + Map.of(gs("three"), 3.0, gs("two"), 2.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Incorrect range as start > stop. + assertEquals(0, client.zrangestore(destination, source, new RangeByIndex(3, 1)).get()); + assertEquals(Map.of(), client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Non-existing source. + assertEquals(0, client.zrangestore(destination, key, new RangeByIndex(0, -1)).get()); + assertEquals(Map.of(), client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zrangestore(destination, key, new RangeByIndex(3, 1)).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -2006,6 +4527,75 @@ public void zrangestore_by_index(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrangestore_binary_by_score(BaseClient client) { + GlideString key = gs("{testKey}:" + UUID.randomUUID()); + GlideString destination = gs("{testKey}:" + UUID.randomUUID()); + GlideString source = gs("{testKey}:" + UUID.randomUUID()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + assertEquals(3, client.zadd(source, membersScores).get()); + + // Range from negative infinity to 3 (exclusive). + RangeByScore query = + new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertEquals(2, client.zrangestore(destination, source, query).get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from 1 (inclusive) to positive infinity. + query = new RangeByScore(new ScoreBoundary(1), InfScoreBound.POSITIVE_INFINITY); + assertEquals(3, client.zrangestore(destination, source, query).get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from negative to positive infinity. Limited to ranks 1 to 2. + query = + new RangeByScore( + InfScoreBound.NEGATIVE_INFINITY, InfScoreBound.POSITIVE_INFINITY, new Limit(1, 2)); + assertEquals(2, client.zrangestore(destination, source, query).get()); + assertEquals( + Map.of(gs("two"), 2.0, gs("three"), 3.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from positive to negative infinity with rev set to true. Limited to ranks 1 to 2. + query = + new RangeByScore( + InfScoreBound.POSITIVE_INFINITY, InfScoreBound.NEGATIVE_INFINITY, new Limit(1, 2)); + assertEquals(2, client.zrangestore(destination, source, query, true).get()); + assertEquals( + Map.of(gs("two"), 2.0, gs("one"), 1.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Incorrect range as start > stop. + query = new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); + assertEquals(0, client.zrangestore(destination, source, query).get()); + assertEquals(Map.of(), client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Non-existent source. + query = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertEquals(0, client.zrangestore(destination, key, query).get()); + assertEquals(Map.of(), client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zrangestore( + destination, + key, + new RangeByScore(new ScoreBoundary(0), new ScoreBoundary(3))) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -2074,6 +4664,68 @@ public void zrangestore_by_score(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrangestore_binary_by_lex(BaseClient client) { + GlideString key = gs("{testKey}:" + UUID.randomUUID()); + GlideString destination = gs("{testKey}:" + UUID.randomUUID()); + GlideString source = gs("{testKey}:" + UUID.randomUUID()); + Map membersScores = Map.of(gs("a"), 1.0, gs("b"), 2.0, gs("c"), 3.0); + assertEquals(3, client.zadd(source, membersScores).get()); + + // Range from negative infinity to "c" (exclusive). + RangeByLex query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + assertEquals(2, client.zrangestore(destination, source, query).get()); + assertEquals( + Map.of(gs("a"), 1.0, gs("b"), 2.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from "a" (inclusive) to positive infinity. + query = new RangeByLex(new LexBoundary("a"), InfLexBound.POSITIVE_INFINITY); + assertEquals(3, client.zrangestore(destination, source, query).get()); + assertEquals( + Map.of(gs("a"), 1.0, gs("b"), 2.0, gs("c"), 3.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from negative to positive infinity. Limited to ranks 1 to 2. + query = + new RangeByLex( + InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY, new Limit(1, 2)); + assertEquals(2, client.zrangestore(destination, source, query).get()); + assertEquals( + Map.of(gs("b"), 2.0, gs("c"), 3.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Range from positive to negative infinity with rev set to true. Limited to ranks 1 to 2. + query = + new RangeByLex( + InfLexBound.POSITIVE_INFINITY, InfLexBound.NEGATIVE_INFINITY, new Limit(1, 2)); + assertEquals(2, client.zrangestore(destination, source, query, true).get()); + assertEquals( + Map.of(gs("b"), 2.0, gs("a"), 1.0), + client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Non-existent source. + query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + assertEquals(0, client.zrangestore(destination, key, query).get()); + assertEquals(Map.of(), client.zrangeWithScores(destination, new RangeByIndex(0, -1)).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zrangestore( + destination, + key, + new RangeByLex(new LexBoundary("a"), new LexBoundary("c"))) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -2139,441 +4791,10003 @@ public void zrangestore_by_lex(BaseClient client) { @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void xadd(BaseClient client) { - String key = UUID.randomUUID().toString(); - String field1 = UUID.randomUUID().toString(); - String field2 = UUID.randomUUID().toString(); + public void zunionstore(BaseClient client) { + String key1 = "{testKey}:1-" + UUID.randomUUID(); + String key2 = "{testKey}:2-" + UUID.randomUUID(); + String key3 = "{testKey}:3-" + UUID.randomUUID(); + String key4 = "{testKey}:4-" + UUID.randomUUID(); + RangeByIndex query = new RangeByIndex(0, -1); + Map membersScores1 = Map.of("one", 1.0, "two", 2.0); + Map membersScores2 = Map.of("two", 2.5, "three", 3.0); - assertNull( + assertEquals(2, client.zadd(key1, membersScores1).get()); + assertEquals(2, client.zadd(key2, membersScores2).get()); + + assertEquals(3, client.zunionstore(key3, new KeyArray(new String[] {key1, key2})).get()); + assertEquals( + Map.of("one", 1.0, "two", 4.5, "three", 3.0), client.zrangeWithScores(key3, query).get()); + + // Union results are aggregated by the max score of elements + assertEquals( + 3, client.zunionstore(key3, new KeyArray(new String[] {key1, key2}), Aggregate.MAX).get()); + assertEquals( + Map.of("one", 1.0, "two", 2.5, "three", 3.0), client.zrangeWithScores(key3, query).get()); + + // Union results are aggregated by the min score of elements + assertEquals( + 3, client.zunionstore(key3, new KeyArray(new String[] {key1, key2}), Aggregate.MIN).get()); + assertEquals( + Map.of("one", 1.0, "two", 2.0, "three", 3.0), client.zrangeWithScores(key3, query).get()); + + // Union results are aggregated by the sum of the scores of elements + assertEquals( + 3, client.zunionstore(key3, new KeyArray(new String[] {key1, key2}), Aggregate.SUM).get()); + assertEquals( + Map.of("one", 1.0, "two", 4.5, "three", 3.0), client.zrangeWithScores(key3, query).get()); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + 3, client - .xadd( - key, - Map.of(field1, "foo0", field2, "bar0"), - StreamAddOptions.builder().makeStream(Boolean.FALSE).build()) + .zunionstore(key3, new WeightedKeys(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) .get()); + assertEquals( + Map.of("one", 2.0, "two", 9.0, "three", 6.0), client.zrangeWithScores(key3, query).get()); - String timestamp1 = "0-1"; + // Union results are aggregated by the maximum score, with scores for key1 multiplied by 1.0 and + // for key2 by 2.0. assertEquals( - timestamp1, + 3, client - .xadd( - key, - Map.of(field1, "foo1", field2, "bar1"), - StreamAddOptions.builder().id(timestamp1).build()) + .zunionstore( + key3, + new WeightedKeys(List.of(Pair.of(key1, 1.0), Pair.of(key2, 2.0))), + Aggregate.MAX) .get()); + assertEquals( + Map.of("one", 1.0, "two", 5.0, "three", 6.0), client.zrangeWithScores(key3, query).get()); - assertNotNull(client.xadd(key, Map.of(field1, "foo2", field2, "bar2")).get()); - // TODO update test when XLEN is available - if (client instanceof RedisClient) { - assertEquals(2L, ((RedisClient) client).customCommand(new String[] {"XLEN", key}).get()); - } else if (client instanceof RedisClusterClient) { - assertEquals( - 2L, - ((RedisClusterClient) client) - .customCommand(new String[] {"XLEN", key}) - .get() - .getSingleValue()); - } + // Key exists, but it is not a set + assertEquals(OK, client.set(key4, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zunionstore(key3, new KeyArray(new String[] {key4, key2})).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } - // this will trim the first entry. - String id = + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zunionstore_binary(BaseClient client) { + GlideString key1 = gs("{testKey}:1-" + UUID.randomUUID()); + GlideString key2 = gs("{testKey}:2-" + UUID.randomUUID()); + GlideString key3 = gs("{testKey}:3-" + UUID.randomUUID()); + GlideString key4 = gs("{testKey}:4-" + UUID.randomUUID()); + RangeByIndex query = new RangeByIndex(0, -1); + Map membersScores1 = Map.of("one", 1.0, "two", 2.0); + Map membersScores2 = Map.of("two", 2.5, "three", 3.0); + + assertEquals( + 2, client - .xadd( - key, - Map.of(field1, "foo3", field2, "bar3"), - StreamAddOptions.builder() - .trim(new StreamAddOptions.MaxLen(Boolean.TRUE, 2L)) - .build()) - .get(); - assertNotNull(id); - // TODO update test when XLEN is available - if (client instanceof RedisClient) { - assertEquals(2L, ((RedisClient) client).customCommand(new String[] {"XLEN", key}).get()); - } else if (client instanceof RedisClusterClient) { - assertEquals( - 2L, - ((RedisClusterClient) client) - .customCommand(new String[] {"XLEN", key}) - .get() - .getSingleValue()); - } + .zadd(key1.toString(), membersScores1) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals( + 2, + client + .zadd(key2.toString(), membersScores2) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged - // this will trim the second entry. - assertNotNull( + assertEquals( + 3, client.zunionstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2})).get()); + assertEquals( + Map.of("one", 1.0, "two", 4.5, "three", 3.0), client - .xadd( - key, - Map.of(field1, "foo4", field2, "bar4"), - StreamAddOptions.builder() - .trim(new StreamAddOptions.MinId(Boolean.TRUE, id)) - .build()) + .zrangeWithScores(key3.toString(), query) + .get()); // TODO: use the binary version of this function call once the binary version + // of zrangeWithScores() is merged + + // Union results are aggregated by the max score of elements + assertEquals( + 3, + client + .zunionstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MAX) .get()); - // TODO update test when XLEN is available - if (client instanceof RedisClient) { - assertEquals(2L, ((RedisClient) client).customCommand(new String[] {"XLEN", key}).get()); - } else if (client instanceof RedisClusterClient) { - assertEquals( - 2L, - ((RedisClusterClient) client) - .customCommand(new String[] {"XLEN", key}) - .get() - .getSingleValue()); - } + assertEquals( + Map.of("one", 1.0, "two", 2.5, "three", 3.0), + client + .zrangeWithScores(key3.toString(), query) + .get()); // TODO: use the binary version of this function call once the binary version + // of zrangeWithScores() is merged - /** - * TODO add test to XTRIM on maxlen expect( await client.xtrim(key, { method: "maxlen", - * threshold: 1, exact: true, }), ).toEqual(1); expect(await client.customCommand(["XLEN", - * key])).toEqual(1); - */ - } + // Union results are aggregated by the min score of elements + assertEquals( + 3, + client + .zunionstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MIN) + .get()); + assertEquals( + Map.of("one", 1.0, "two", 2.0, "three", 3.0), + client + .zrangeWithScores(key3.toString(), query) + .get()); // TODO: use the binary version of this function call once the binary version + // of zrangeWithScores() is merged - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void type(BaseClient client) { - String nonExistingKey = UUID.randomUUID().toString(); - String stringKey = UUID.randomUUID().toString(); - String listKey = UUID.randomUUID().toString(); - String hashKey = UUID.randomUUID().toString(); - String setKey = UUID.randomUUID().toString(); - String zsetKey = UUID.randomUUID().toString(); - String streamKey = UUID.randomUUID().toString(); + // Union results are aggregated by the sum of the scores of elements + assertEquals( + 3, + client + .zunionstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.SUM) + .get()); + assertEquals( + Map.of("one", 1.0, "two", 4.5, "three", 3.0), + client + .zrangeWithScores(key3.toString(), query) + .get()); // TODO: use the binary version of this function call once the binary version + // of zrangeWithScores() is merged - assertEquals(OK, client.set(stringKey, "value").get()); - assertEquals(1, client.lpush(listKey, new String[] {"value"}).get()); - assertEquals(1, client.hset(hashKey, Map.of("1", "2")).get()); - assertEquals(1, client.sadd(setKey, new String[] {"value"}).get()); - assertEquals(1, client.zadd(zsetKey, Map.of("1", 2d)).get()); - assertNotNull(client.xadd(streamKey, Map.of("field", "value"))); + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + 3, + client + .zunionstore( + key3, new WeightedKeysBinary(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); + assertEquals( + Map.of("one", 2.0, "two", 9.0, "three", 6.0), + client + .zrangeWithScores(key3.toString(), query) + .get()); // TODO: use the binary version of this function call once the binary version + // of zrangeWithScores() is merged - assertTrue("none".equalsIgnoreCase(client.type(nonExistingKey).get())); - assertTrue("string".equalsIgnoreCase(client.type(stringKey).get())); - assertTrue("list".equalsIgnoreCase(client.type(listKey).get())); - assertTrue("hash".equalsIgnoreCase(client.type(hashKey).get())); - assertTrue("set".equalsIgnoreCase(client.type(setKey).get())); - assertTrue("zset".equalsIgnoreCase(client.type(zsetKey).get())); - assertTrue("stream".equalsIgnoreCase(client.type(streamKey).get())); + // Union results are aggregated by the maximum score, with scores for key1 multiplied by 1.0 and + // for key2 by 2.0. + assertEquals( + 3, + client + .zunionstore( + key3, + new WeightedKeysBinary(List.of(Pair.of(key1, 1.0), Pair.of(key2, 2.0))), + Aggregate.MAX) + .get()); + assertEquals( + Map.of("one", 1.0, "two", 5.0, "three", 6.0), + client + .zrangeWithScores(key3.toString(), query) + .get()); // TODO: use the binary version of this function call once the binary version + // of zrangeWithScores() is merged + + // Key exists, but it is not a set + assertEquals(OK, client.set(key4, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client.zunionstore(key3, new KeyArrayBinary(new GlideString[] {key4, key2})).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void linsert(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); + public void zunion(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); - assertEquals(4, client.lpush(key1, new String[] {"4", "3", "2", "1"}).get()); - assertEquals(5, client.linsert(key1, BEFORE, "2", "1.5").get()); - assertEquals(6, client.linsert(key1, AFTER, "3", "3.5").get()); + String key1 = "{testKey}:1-" + UUID.randomUUID(); + String key2 = "{testKey}:2-" + UUID.randomUUID(); + String key3 = "{testKey}:3-" + UUID.randomUUID(); + Map membersScores1 = Map.of("one", 1.0, "two", 2.0); + Map membersScores2 = Map.of("two", 3.5, "three", 3.0); + + assertEquals(2, client.zadd(key1, membersScores1).get()); + assertEquals(2, client.zadd(key2, membersScores2).get()); + + // Union results are aggregated by the sum of the scores of elements by default assertArrayEquals( - new String[] {"1", "1.5", "2", "3", "3.5", "4"}, client.lrange(key1, 0, -1).get()); + new String[] {"one", "three", "two"}, + client.zunion(new KeyArray(new String[] {key1, key2})).get()); + assertEquals( + Map.of("one", 1.0, "three", 3.0, "two", 5.5), + client.zunionWithScores(new KeyArray(new String[] {key1, key2})).get()); - assertEquals(0, client.linsert(key2, BEFORE, "pivot", "elem").get()); - assertEquals(-1, client.linsert(key1, AFTER, "5", "6").get()); + // Union results are aggregated by the max score of elements + assertEquals( + Map.of("one", 1.0, "three", 3.0, "two", 3.5), + client.zunionWithScores(new KeyArray(new String[] {key1, key2}), Aggregate.MAX).get()); - // Key exists, but it is not a list - assertEquals(OK, client.set(key2, "linsert").get()); - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.linsert(key2, AFTER, "p", "e").get()); - assertTrue(executionException.getCause() instanceof RequestException); - } + // Union results are aggregated by the min score of elements + assertEquals( + Map.of("one", 1.0, "two", 2.0, "three", 3.0), + client.zunionWithScores(new KeyArray(new String[] {key1, key2}), Aggregate.MIN).get()); - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void brpop(BaseClient client) { - String listKey1 = "{listKey}-1-" + UUID.randomUUID(); - String listKey2 = "{listKey}-2-" + UUID.randomUUID(); - String value1 = "value1-" + UUID.randomUUID(); - String value2 = "value2-" + UUID.randomUUID(); - assertEquals(2, client.lpush(listKey1, new String[] {value1, value2}).get()); + // Union results are aggregated by the sum of the scores of elements + assertEquals( + Map.of("one", 1.0, "three", 3.0, "two", 5.5), + client.zunionWithScores(new KeyArray(new String[] {key1, key2}), Aggregate.SUM).get()); - var response = client.brpop(new String[] {listKey1, listKey2}, 0.5).get(); - assertArrayEquals(new String[] {listKey1, value1}, response); + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + Map.of("one", 2.0, "three", 6.0, "two", 11.0), + client + .zunionWithScores(new WeightedKeys(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); - // nothing popped out - assertNull( + // Union results are aggregated by the minimum score, with scores for key1 multiplied by 1.0 and + // for key2 by -2.0. + assertEquals( + Map.of("two", -7.0, "three", -6.0, "one", 1.0), client - .brpop(new String[] {listKey2}, REDIS_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .zunionWithScores( + new WeightedKeys(List.of(Pair.of(key1, 1.0), Pair.of(key2, -2.0))), Aggregate.MIN) .get()); - // Key exists, but it is not a list - assertEquals(OK, client.set("foo", "bar").get()); + // Non Existing Key + assertArrayEquals( + new String[] {"one", "two"}, client.zunion(new KeyArray(new String[] {key1, key3})).get()); + assertEquals( + Map.of("one", 1.0, "two", 2.0), + client.zunionWithScores(new KeyArray(new String[] {key1, key3})).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key3, "value").get()); ExecutionException executionException = assertThrows( - ExecutionException.class, () -> client.brpop(new String[] {"foo"}, .0001).get()); + ExecutionException.class, + () -> client.zunion(new KeyArray(new String[] {key1, key3})).get()); assertTrue(executionException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void rpushx(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); - String key3 = UUID.randomUUID().toString(); + public void zunion_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); - assertEquals(1, client.rpush(key1, new String[] {"0"}).get()); - assertEquals(4, client.rpushx(key1, new String[] {"1", "2", "3"}).get()); - assertArrayEquals(new String[] {"0", "1", "2", "3"}, client.lrange(key1, 0, -1).get()); + GlideString key1 = gs("{testKey}:1-" + UUID.randomUUID()); + GlideString key2 = gs("{testKey}:2-" + UUID.randomUUID()); + GlideString key3 = gs("{testKey}:3-" + UUID.randomUUID()); + Map membersScores1 = Map.of("one", 1.0, "two", 2.0); + Map membersScores2 = Map.of("two", 3.5, "three", 3.0); - assertEquals(0, client.rpushx(key2, new String[] {"1"}).get()); - assertArrayEquals(new String[0], client.lrange(key2, 0, -1).get()); + assertEquals( + 2, + client + .zadd(key1.toString(), membersScores1) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals( + 2, + client + .zadd(key2.toString(), membersScores2) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged - // Key exists, but it is not a list - assertEquals(OK, client.set(key3, "bar").get()); + // Union results are aggregated by the sum of the scores of elements by default + assertArrayEquals( + new GlideString[] {gs("one"), gs("three"), gs("two")}, + client.zunion(new KeyArrayBinary(new GlideString[] {key1, key2})).get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("three"), 3.0, gs("two"), 5.5), + client.zunionWithScores(new KeyArrayBinary(new GlideString[] {key1, key2})).get()); + + // Union results are aggregated by the max score of elements + assertEquals( + Map.of(gs("one"), 1.0, gs("three"), 3.0, gs("two"), 3.5), + client + .zunionWithScores(new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MAX) + .get()); + + // Union results are aggregated by the min score of elements + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0), + client + .zunionWithScores(new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MIN) + .get()); + + // Union results are aggregated by the sum of the scores of elements + assertEquals( + Map.of(gs("one"), 1.0, gs("three"), 3.0, gs("two"), 5.5), + client + .zunionWithScores(new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.SUM) + .get()); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + Map.of(gs("one"), 2.0, gs("three"), 6.0, gs("two"), 11.0), + client + .zunionWithScores( + new WeightedKeysBinary(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); + + // Union results are aggregated by the minimum score, with scores for key1 multiplied by 1.0 and + // for key2 by -2.0. + assertEquals( + Map.of(gs("two"), -7.0, gs("three"), -6.0, gs("one"), 1.0), + client + .zunionWithScores( + new WeightedKeysBinary(List.of(Pair.of(key1, 1.0), Pair.of(key2, -2.0))), + Aggregate.MIN) + .get()); + + // Non Existing Key + assertArrayEquals( + new GlideString[] {gs("one"), gs("two")}, + client.zunion(new KeyArrayBinary(new GlideString[] {key1, key3})).get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0), + client.zunionWithScores(new KeyArrayBinary(new GlideString[] {key1, key3})).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key3, gs("value")).get()); ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.rpushx(key3, new String[] {"_"}).get()); - assertTrue(executionException.getCause() instanceof RequestException); - // empty element list - executionException = - assertThrows(ExecutionException.class, () -> client.rpushx(key2, new String[0]).get()); + assertThrows( + ExecutionException.class, + () -> client.zunion(new KeyArrayBinary(new GlideString[] {key1, key3})).get()); assertTrue(executionException.getCause() instanceof RequestException); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void blpop(BaseClient client) { - String listKey1 = "{listKey}-1-" + UUID.randomUUID(); - String listKey2 = "{listKey}-2-" + UUID.randomUUID(); - String value1 = "value1-" + UUID.randomUUID(); - String value2 = "value2-" + UUID.randomUUID(); - assertEquals(2, client.lpush(listKey1, new String[] {value1, value2}).get()); + public void zinterstore_binary(BaseClient client) { + GlideString key1 = gs("{testKey}:1-" + UUID.randomUUID()); + GlideString key2 = gs("{testKey}:2-" + UUID.randomUUID()); + GlideString key3 = gs("{testKey}:3-" + UUID.randomUUID()); + GlideString key4 = gs("{testKey}:4-" + UUID.randomUUID()); + RangeByIndex query = new RangeByIndex(0, -1); + Map membersScores1 = Map.of(gs("one"), 1.0, gs("two"), 2.0); + Map membersScores2 = + Map.of(gs("one"), 1.5, gs("two"), 2.5, gs("three"), 3.5); - var response = client.blpop(new String[] {listKey1, listKey2}, 0.5).get(); - assertArrayEquals(new String[] {listKey1, value2}, response); + assertEquals(2, client.zadd(key1, membersScores1).get()); + assertEquals(3, client.zadd(key2, membersScores2).get()); - // nothing popped out - assertNull( + assertEquals( + 2, client.zinterstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2})).get()); + assertEquals( + Map.of(gs("one"), 2.5, gs("two"), 4.5), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the max score of elements + assertEquals( + 2, client - .blpop(new String[] {listKey2}, REDIS_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .zinterstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MAX) .get()); + assertEquals( + Map.of(gs("one"), 1.5, gs("two"), 2.5), client.zrangeWithScores(key3, query).get()); - // Key exists, but it is not a list - assertEquals(OK, client.set("foo", "bar").get()); + // Intersection results are aggregated by the min score of elements + assertEquals( + 2, + client + .zinterstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MIN) + .get()); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the sum of the scores of elements + assertEquals( + 2, + client + .zinterstore(key3, new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.SUM) + .get()); + assertEquals( + Map.of(gs("one"), 2.5, gs("two"), 4.5), client.zrangeWithScores(key3, query).get()); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + 2, + client + .zinterstore( + key3, new WeightedKeysBinary(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); + assertEquals( + Map.of(gs("one"), 5.0, gs("two"), 9.0), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the minimum score, with scores for key1 multiplied by + // 1.0 and for key2 by -2.0. + assertEquals( + 2, + client + .zinterstore( + key3, + new WeightedKeysBinary(List.of(Pair.of(key1, 1.0), Pair.of(key2, -2.0))), + Aggregate.MIN) + .get()); + assertEquals( + Map.of(gs("two"), -5.0, gs("one"), -3.0), client.zrangeWithScores(key3, query).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key4, gs("value")).get()); ExecutionException executionException = assertThrows( - ExecutionException.class, () -> client.blpop(new String[] {"foo"}, .0001).get()); + ExecutionException.class, + () -> + client.zinterstore(key3, new KeyArrayBinary(new GlideString[] {key4, key2})).get()); assertTrue(executionException.getCause() instanceof RequestException); } - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void lpushx(BaseClient client) { - String key1 = UUID.randomUUID().toString(); - String key2 = UUID.randomUUID().toString(); - String key3 = UUID.randomUUID().toString(); + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zinter(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + + String key1 = "{testKey}:1-" + UUID.randomUUID(); + String key2 = "{testKey}:2-" + UUID.randomUUID(); + String key3 = "{testKey}:3-" + UUID.randomUUID(); + Map membersScores1 = Map.of("one", 1.0, "two", 2.0); + Map membersScores2 = Map.of("two", 3.5, "three", 3.0); + + assertEquals(2, client.zadd(key1, membersScores1).get()); + assertEquals(2, client.zadd(key2, membersScores2).get()); + + // Intersection results are aggregated by the sum of the scores of elements by default + assertArrayEquals( + new String[] {"two"}, client.zinter(new KeyArray(new String[] {key1, key2})).get()); + assertEquals( + Map.of("two", 5.5), client.zinterWithScores(new KeyArray(new String[] {key1, key2})).get()); + + // Intersection results are aggregated by the max score of elements + assertEquals( + Map.of("two", 3.5), + client.zinterWithScores(new KeyArray(new String[] {key1, key2}), Aggregate.MAX).get()); + + // Intersection results are aggregated by the min score of elements + assertEquals( + Map.of("two", 2.0), + client.zinterWithScores(new KeyArray(new String[] {key1, key2}), Aggregate.MIN).get()); + + // Intersection results are aggregated by the sum of the scores of elements + assertEquals( + Map.of("two", 5.5), + client.zinterWithScores(new KeyArray(new String[] {key1, key2}), Aggregate.SUM).get()); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + Map.of("two", 11.0), + client + .zinterWithScores(new WeightedKeys(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); + + // Intersection results are aggregated by the minimum score, + // with scores for key1 multiplied by 1.0 and for key2 by -2.0. + assertEquals( + Map.of("two", -7.0), + client + .zinterWithScores( + new WeightedKeys(List.of(Pair.of(key1, 1.0), Pair.of(key2, -2.0))), Aggregate.MIN) + .get()); + + // Non-existing Key - empty intersection + assertEquals(0, client.zinter(new KeyArray(new String[] {key1, key3})).get().length); + assertEquals(0, client.zinterWithScores(new KeyArray(new String[] {key1, key3})).get().size()); + + // empty key list + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.zinter(new KeyArray(new String[0])).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.zinterWithScores(new WeightedKeys(List.of())).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key3, "value").get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.zinter(new KeyArray(new String[] {key1, key3})).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zinter_binary(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in 6.2.0"); + + GlideString key1 = gs("{testKey}:1-" + UUID.randomUUID()); + GlideString key2 = gs("{testKey}:2-" + UUID.randomUUID()); + GlideString key3 = gs("{testKey}:3-" + UUID.randomUUID()); + Map membersScores1 = Map.of(gs("one"), 1.0, gs("two"), 2.0); + Map membersScores2 = Map.of(gs("two"), 3.5, gs("three"), 3.0); + + assertEquals(2, client.zadd(key1, membersScores1).get()); + assertEquals(2, client.zadd(key2, membersScores2).get()); + + // Intersection results are aggregated by the sum of the scores of elements by default + assertArrayEquals( + new GlideString[] {gs("two")}, + client.zinter(new KeyArrayBinary(new GlideString[] {key1, key2})).get()); + assertEquals( + Map.of(gs("two"), 5.5), + client.zinterWithScores(new KeyArrayBinary(new GlideString[] {key1, key2})).get()); + + // Intersection results are aggregated by the max score of elements + assertEquals( + Map.of(gs("two"), 3.5), + client + .zinterWithScores(new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MAX) + .get()); + + // Intersection results are aggregated by the min score of elements + assertEquals( + Map.of(gs("two"), 2.0), + client + .zinterWithScores(new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.MIN) + .get()); + + // Intersection results are aggregated by the sum of the scores of elements + assertEquals( + Map.of(gs("two"), 5.5), + client + .zinterWithScores(new KeyArrayBinary(new GlideString[] {key1, key2}), Aggregate.SUM) + .get()); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + Map.of(gs("two"), 11.0), + client + .zinterWithScores( + new WeightedKeysBinary(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); + + // Intersection results are aggregated by the minimum score, + // with scores for key1 multiplied by 1.0 and for key2 by -2.0. + assertEquals( + Map.of(gs("two"), -7.0), + client + .zinterWithScores( + new WeightedKeysBinary(List.of(Pair.of(key1, 1.0), Pair.of(key2, -2.0))), + Aggregate.MIN) + .get()); + + // Non-existing Key - empty intersection + assertEquals(0, client.zinter(new KeyArrayBinary(new GlideString[] {key1, key3})).get().length); + assertEquals( + 0, + client.zinterWithScores(new KeyArrayBinary(new GlideString[] {key1, key3})).get().size()); + + // empty key list + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zinter(new KeyArrayBinary(new GlideString[0])).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.zinterWithScores(new WeightedKeysBinary(List.of())).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key3, gs("value")).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.zinter(new KeyArrayBinary(new GlideString[] {key1, key3})).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zintercard(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")); + String key1 = "{zintercard}-" + UUID.randomUUID(); + String key2 = "{zintercard}-" + UUID.randomUUID(); + String key3 = "{zintercard}-" + UUID.randomUUID(); + + assertEquals(3, client.zadd(key1, Map.of("a", 1.0, "b", 2.0, "c", 3.0)).get()); + assertEquals(3, client.zadd(key2, Map.of("b", 1.0, "c", 2.0, "d", 3.0)).get()); + + assertEquals(2L, client.zintercard(new String[] {key1, key2}).get()); + assertEquals(0, client.zintercard(new String[] {key1, key3}).get()); + + assertEquals(2L, client.zintercard(new String[] {key1, key2}, 0).get()); + assertEquals(1L, client.zintercard(new String[] {key1, key2}, 1).get()); + assertEquals(2L, client.zintercard(new String[] {key1, key2}, 3).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key3, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zintercard(new String[] {key3}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // incorrect arguments + executionException = + assertThrows(ExecutionException.class, () -> client.zintercard(new String[0]).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.zintercard(new String[0], 42).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zinterstore(BaseClient client) { + String key1 = "{testKey}:1-" + UUID.randomUUID(); + String key2 = "{testKey}:2-" + UUID.randomUUID(); + String key3 = "{testKey}:3-" + UUID.randomUUID(); + String key4 = "{testKey}:4-" + UUID.randomUUID(); + RangeByIndex query = new RangeByIndex(0, -1); + Map membersScores1 = Map.of("one", 1.0, "two", 2.0); + Map membersScores2 = Map.of("one", 1.5, "two", 2.5, "three", 3.5); + + assertEquals(2, client.zadd(key1, membersScores1).get()); + assertEquals(3, client.zadd(key2, membersScores2).get()); + + assertEquals(2, client.zinterstore(key3, new KeyArray(new String[] {key1, key2})).get()); + assertEquals(Map.of("one", 2.5, "two", 4.5), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the max score of elements + assertEquals( + 2, client.zinterstore(key3, new KeyArray(new String[] {key1, key2}), Aggregate.MAX).get()); + assertEquals(Map.of("one", 1.5, "two", 2.5), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the min score of elements + assertEquals( + 2, client.zinterstore(key3, new KeyArray(new String[] {key1, key2}), Aggregate.MIN).get()); + assertEquals(Map.of("one", 1.0, "two", 2.0), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the sum of the scores of elements + assertEquals( + 2, client.zinterstore(key3, new KeyArray(new String[] {key1, key2}), Aggregate.SUM).get()); + assertEquals(Map.of("one", 2.5, "two", 4.5), client.zrangeWithScores(key3, query).get()); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + assertEquals( + 2, + client + .zinterstore(key3, new WeightedKeys(List.of(Pair.of(key1, 2.0), Pair.of(key2, 2.0)))) + .get()); + assertEquals(Map.of("one", 5.0, "two", 9.0), client.zrangeWithScores(key3, query).get()); + + // Intersection results are aggregated by the minimum score, with scores for key1 multiplied by + // 1.0 and for key2 by -2.0. + assertEquals( + 2, + client + .zinterstore( + key3, + new WeightedKeys(List.of(Pair.of(key1, 1.0), Pair.of(key2, -2.0))), + Aggregate.MIN) + .get()); + assertEquals(Map.of("two", -5.0, "one", -3.0), client.zrangeWithScores(key3, query).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key4, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.zinterstore(key3, new KeyArray(new String[] {key4, key2})).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zmpop(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + String key1 = "{zmpop}-1-" + UUID.randomUUID(); + String key2 = "{zmpop}-2-" + UUID.randomUUID(); + String key3 = "{zmpop}-3-" + UUID.randomUUID(); + + assertEquals(2, client.zadd(key1, Map.of("a1", 1., "b1", 2.)).get()); + assertEquals(2, client.zadd(key2, Map.of("a2", .1, "b2", .2)).get()); + + assertArrayEquals( + new Object[] {key1, Map.of("b1", 2.)}, client.zmpop(new String[] {key1, key2}, MAX).get()); + assertArrayEquals( + new Object[] {key2, Map.of("b2", .2, "a2", .1)}, + client.zmpop(new String[] {key2, key1}, MAX, 10).get()); + + // nothing popped out + assertNull(client.zmpop(new String[] {key3}, MIN).get()); + assertNull(client.zmpop(new String[] {key3}, MIN, 1).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, "value").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zmpop(new String[] {key3}, MAX).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.zmpop(new String[] {key3}, MAX, 1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // incorrect argument + executionException = + assertThrows( + ExecutionException.class, () -> client.zmpop(new String[] {key1}, MAX, 0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.zmpop(new String[0], MAX).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // check that order of entries in the response is preserved + var entries = new LinkedHashMap(); + for (int i = 0; i < 10; i++) { + // a => 1., b => 2. etc + entries.put("" + ('a' + i), (double) i); + } + assertEquals(10, client.zadd(key2, entries).get()); + assertEquals(entries, client.zmpop(new String[] {key2}, MIN, 10).get()[1]); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zmpop_binary(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + GlideString key1 = gs("{zmpop}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{zmpop}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{zmpop}-3-" + UUID.randomUUID()); + + assertEquals( + 2, + client + .zadd(key1.toString(), Map.of("a1", 1., "b1", 2.)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals( + 2, + client + .zadd(key2.toString(), Map.of("a2", .1, "b2", .2)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + + assertArrayEquals( + new Object[] {key1, Map.of(gs("b1"), 2.)}, + client.zmpop(new GlideString[] {key1, key2}, MAX).get()); + assertArrayEquals( + new Object[] {key2, Map.of(gs("b2"), .2, gs("a2"), .1)}, + client.zmpop(new GlideString[] {key2, key1}, MAX, 10).get()); + + // nothing popped out + assertNull(client.zmpop(new GlideString[] {key3}, MIN).get()); + assertNull(client.zmpop(new GlideString[] {key3}, MIN, 1).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.zmpop(new GlideString[] {key3}, MAX).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.zmpop(new GlideString[] {key3}, MAX, 1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // incorrect argument + executionException = + assertThrows( + ExecutionException.class, () -> client.zmpop(new GlideString[] {key1}, MAX, 0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.zmpop(new GlideString[0], MAX).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // check that order of entries in the response is preserved + var entries = + new LinkedHashMap< + String, Double>(); // TODO: remove this once the binary version of zadd() is merged + var entries_gs = new LinkedHashMap(); + for (int i = 0; i < 10; i++) { + // a => 1., b => 2. etc + entries.put( + "" + ('a' + i), + (double) i); // TODO: remove this once the binary version of zadd() is merged + entries_gs.put(gs("" + ('a' + i)), (double) i); + } + assertEquals( + 10, + client + .zadd(key2.toString(), entries) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals(entries_gs, client.zmpop(new GlideString[] {key2}, MIN, 10).get()[1]); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzmpop(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + String key1 = "{bzmpop}-1-" + UUID.randomUUID(); + String key2 = "{bzmpop}-2-" + UUID.randomUUID(); + String key3 = "{bzmpop}-3-" + UUID.randomUUID(); + + assertEquals(2, client.zadd(key1, Map.of("a1", 1., "b1", 2.)).get()); + assertEquals(2, client.zadd(key2, Map.of("a2", .1, "b2", .2)).get()); + + assertArrayEquals( + new Object[] {key1, Map.of("b1", 2.)}, + client.bzmpop(new String[] {key1, key2}, MAX, 0.1).get()); + assertArrayEquals( + new Object[] {key2, Map.of("b2", .2, "a2", .1)}, + client.bzmpop(new String[] {key2, key1}, MAX, 0.1, 10).get()); + + // nothing popped out + assertNull(client.bzmpop(new String[] {key3}, MIN, 0.001).get()); + assertNull(client.bzmpop(new String[] {key3}, MIN, 0.001, 1).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, "value").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.bzmpop(new String[] {key3}, MAX, .1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.bzmpop(new String[] {key3}, MAX, .1, 1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // incorrect argument + executionException = + assertThrows( + ExecutionException.class, () -> client.bzmpop(new String[] {key1}, MAX, .1, 0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // check that order of entries in the response is preserved + var entries = new LinkedHashMap(); + for (int i = 0; i < 10; i++) { + // a => 1., b => 2. etc + entries.put("" + ('a' + i), (double) i); + } + assertEquals(10, client.zadd(key2, entries).get()); + assertEquals(entries, client.bzmpop(new String[] {key2}, MIN, .1, 10).get()[1]); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzmpop_binary(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + GlideString key1 = gs("{bzmpop}-1-" + UUID.randomUUID()); + GlideString key2 = gs("{bzmpop}-2-" + UUID.randomUUID()); + GlideString key3 = gs("{bzmpop}-3-" + UUID.randomUUID()); + + assertEquals( + 2, + client + .zadd(key1.toString(), Map.of("a1", 1., "b1", 2.)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals( + 2, + client + .zadd(key2.toString(), Map.of("a2", .1, "b2", .2)) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + + assertArrayEquals( + new Object[] {key1, Map.of(gs("b1"), 2.)}, + client.bzmpop(new GlideString[] {key1, key2}, MAX, 0.1).get()); + assertArrayEquals( + new Object[] {key2, Map.of(gs("b2"), .2, gs("a2"), .1)}, + client.bzmpop(new GlideString[] {key2, key1}, MAX, 0.1, 10).get()); + + // nothing popped out + assertNull(client.bzmpop(new GlideString[] {key3}, MIN, 0.001).get()); + assertNull(client.bzmpop(new GlideString[] {key3}, MIN, 0.001, 1).get()); + + // Key exists, but it is not a sorted set + assertEquals(OK, client.set(key3, gs("value")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.bzmpop(new GlideString[] {key3}, MAX, .1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.bzmpop(new GlideString[] {key3}, MAX, .1, 1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // incorrect argument + executionException = + assertThrows( + ExecutionException.class, + () -> client.bzmpop(new GlideString[] {key1}, MAX, .1, 0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // check that order of entries in the response is preserved + var entries = + new LinkedHashMap< + String, Double>(); // TODO: remove this line once the binary version of zadd() is merged + var entries_gs = new LinkedHashMap(); + for (int i = 0; i < 10; i++) { + // a => 1., b => 2. etc + entries.put( + "" + ('a' + i), + (double) i); // TODO: remove this line once the binary version of zadd() is merged + entries_gs.put(gs("" + ('a' + i)), (double) i); + } + assertEquals( + 10, + client + .zadd(key2.toString(), entries) + .get()); // TODO: use the binary version of this function call once the binary version + // of zadd() is merged + assertEquals(entries_gs, client.bzmpop(new GlideString[] {key2}, MIN, .1, 10).get()[1]); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzmpop_timeout_check(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + String key = UUID.randomUUID().toString(); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.bzmpop(new String[] {key}, MAX, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.bzmpop(new String[] {key}, MAX, 0).get(3, TimeUnit.SECONDS)); + } + } + + // TODO: add binary version + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bzmpop_binary_timeout_check(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + GlideString key = gs(UUID.randomUUID().toString()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.bzmpop(new GlideString[] {key}, MAX, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.bzmpop(new GlideString[] {key}, MAX, 0).get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_xlen_and_xtrim(BaseClient client) { + String key = UUID.randomUUID().toString(); + String field1 = UUID.randomUUID().toString(); + String field2 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + assertNull( + client + .xadd( + key, + Map.of(field1, "foo0", field2, "bar0"), + StreamAddOptions.builder().makeStream(Boolean.FALSE).build()) + .get()); + + String timestamp1 = "0-1"; + assertEquals( + timestamp1, + client + .xadd( + key, + Map.of(field1, "foo1", field2, "bar1"), + StreamAddOptions.builder().id(timestamp1).build()) + .get()); + + assertNotNull(client.xadd(key, Map.of(field1, "foo2", field2, "bar2")).get()); + assertEquals(2L, client.xlen(key).get()); + + // this will trim the first entry. + String id = + client + .xadd( + key, + Map.of(field1, "foo3", field2, "bar3"), + StreamAddOptions.builder().trim(new MaxLen(true, 2L)).build()) + .get(); + assertNotNull(id); + assertEquals(2L, client.xlen(key).get()); + + // this will trim the second entry. + assertNotNull( + client + .xadd( + key, + Map.of(field1, "foo4", field2, "bar4"), + StreamAddOptions.builder().trim(new MinId(true, id)).build()) + .get()); + assertEquals(2L, client.xlen(key).get()); + + // test xtrim to remove 1 element + assertEquals(1L, client.xtrim(key, new MaxLen(1)).get()); + assertEquals(1L, client.xlen(key).get()); + + // Key does not exist - returns 0 + assertEquals(0L, client.xtrim(key2, new MaxLen(true, 1)).get()); + assertEquals(0L, client.xlen(key2).get()); + + // Throw Exception: Key exists - but it is not a stream + assertEquals(OK, client.set(key2, "xtrimtest").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.xtrim(key2, new MinId("0-1")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + executionException = assertThrows(ExecutionException.class, () -> client.xlen(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xadd_xlen_and_xtrim_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString field1 = gs(UUID.randomUUID().toString()); + GlideString field2 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + assertNull( + client + .xadd( + key, + Map.of(field1, gs("foo0"), field2, gs("bar0")), + StreamAddOptionsBinary.builder().makeStream(Boolean.FALSE).build()) + .get()); + + GlideString timestamp1 = gs("0-1"); + assertEquals( + timestamp1, + client + .xadd( + key, + Map.of(field1, gs("foo1"), field2, gs("bar1")), + StreamAddOptionsBinary.builder().id(timestamp1).build()) + .get()); + + assertNotNull(client.xadd(key, Map.of(field1, gs("foo2"), field2, gs("bar2"))).get()); + assertEquals(2L, client.xlen(key).get()); + + // this will trim the first entry. + GlideString id = + client + .xadd( + key, + Map.of(field1, gs("foo3"), field2, gs("bar3")), + StreamAddOptionsBinary.builder().trim(new MaxLen(true, 2L)).build()) + .get(); + assertNotNull(id); + assertEquals(2L, client.xlen(key).get()); + + // this will trim the second entry. + assertNotNull( + client + .xadd( + key, + Map.of(field1, gs("foo4"), field2, gs("bar4")), + StreamAddOptionsBinary.builder().trim(new MinId(true, id)).build()) + .get()); + assertEquals(2L, client.xlen(key).get()); + + // test xtrim to remove 1 element + assertEquals(1L, client.xtrim(key, new MaxLen(1)).get()); + assertEquals(1L, client.xlen(key).get()); + + // Key does not exist - returns 0 + assertEquals(0L, client.xtrim(key2, new MaxLen(true, 1)).get()); + assertEquals(0L, client.xlen(key2).get()); + + // Throw Exception: Key exists - but it is not a stream + assertEquals(OK, client.set(key2, gs("xtrimtest")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.xtrim(key2, new MinId("0-1")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + executionException = assertThrows(ExecutionException.class, () -> client.xlen(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xread(BaseClient client) { + String key1 = "{key}:1" + UUID.randomUUID(); + String key2 = "{key}:2" + UUID.randomUUID(); + String field1 = "f1_"; + String field2 = "f2_"; + String field3 = "f3_"; + + // setup first entries in streams key1 and key2 + Map timestamp_1_1_map = new LinkedHashMap<>(); + timestamp_1_1_map.put(field1, field1 + "1"); + timestamp_1_1_map.put(field3, field3 + "1"); + String timestamp_1_1 = + client.xadd(key1, timestamp_1_1_map, StreamAddOptions.builder().id("1-1").build()).get(); + assertNotNull(timestamp_1_1); + + String timestamp_2_1 = + client + .xadd(key2, Map.of(field2, field2 + "1"), StreamAddOptions.builder().id("2-1").build()) + .get(); + assertNotNull(timestamp_2_1); + + // setup second entries in streams key1 and key2 + String timestamp_1_2 = + client + .xadd(key1, Map.of(field1, field1 + "2"), StreamAddOptions.builder().id("1-2").build()) + .get(); + assertNotNull(timestamp_1_2); + + String timestamp_2_2 = + client + .xadd(key2, Map.of(field2, field2 + "2"), StreamAddOptions.builder().id("2-2").build()) + .get(); + assertNotNull(timestamp_2_2); + + // setup third entries in streams key1 and key2 + Map timestamp_1_3_map = new LinkedHashMap<>(); + timestamp_1_3_map.put(field1, field1 + "3"); + timestamp_1_3_map.put(field3, field3 + "3"); + String timestamp_1_3 = + client.xadd(key1, timestamp_1_3_map, StreamAddOptions.builder().id("1-3").build()).get(); + assertNotNull(timestamp_1_3); + + String timestamp_2_3 = + client + .xadd(key2, Map.of(field2, field2 + "3"), StreamAddOptions.builder().id("2-3").build()) + .get(); + assertNotNull(timestamp_2_3); + + Map> result = + client.xread(Map.of(key1, timestamp_1_1, key2, timestamp_2_1)).get(); + + // check key1 + Map expected_key1 = new LinkedHashMap<>(); + expected_key1.put(timestamp_1_2, new String[][] {{field1, field1 + "2"}}); + expected_key1.put( + timestamp_1_3, + new String[][] { + {field1, field1 + "3"}, + {field3, field3 + "3"} + }); + assertDeepEquals(expected_key1, result.get(key1)); + + // check key2 + Map expected_key2 = new LinkedHashMap<>(); + expected_key2.put(timestamp_2_2, new String[][] {{field2, field2 + "2"}}); + expected_key2.put(timestamp_2_3, new String[][] {{field2, field2 + "3"}}); + assertDeepEquals(expected_key2, result.get(key2)); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xread_binary(BaseClient client) { + GlideString key1 = gs("{key}:1" + UUID.randomUUID()); + GlideString key2 = gs("{key}:2" + UUID.randomUUID()); + GlideString field1 = gs("f1_"); + GlideString field2 = gs("f2_"); + GlideString field3 = gs("f3_"); + + // setup first entries in streams key1 and key2 + Map timestamp_1_1_map = new LinkedHashMap<>(); + timestamp_1_1_map.put(field1, gs(field1.toString() + "1")); + timestamp_1_1_map.put(field3, gs(field3.toString() + "1")); + GlideString timestamp_1_1 = + client + .xadd(key1, timestamp_1_1_map, StreamAddOptionsBinary.builder().id(gs("1-1")).build()) + .get(); + assertNotNull(timestamp_1_1); + + GlideString timestamp_2_1 = + client + .xadd( + key2, + Map.of(field2, gs(field2.toString() + "1")), + StreamAddOptionsBinary.builder().id(gs("2-1")).build()) + .get(); + assertNotNull(timestamp_2_1); + + // setup second entries in streams key1 and key2 + GlideString timestamp_1_2 = + client + .xadd( + key1, + Map.of(field1, gs(field1.toString() + "2")), + StreamAddOptionsBinary.builder().id(gs("1-2")).build()) + .get(); + assertNotNull(timestamp_1_2); + + GlideString timestamp_2_2 = + client + .xadd( + key2, + Map.of(field2, gs(field2.toString() + "2")), + StreamAddOptionsBinary.builder().id(gs("2-2")).build()) + .get(); + assertNotNull(timestamp_2_2); + + // setup third entries in streams key1 and key2 + Map timestamp_1_3_map = new LinkedHashMap<>(); + timestamp_1_3_map.put(field1, gs(field1.toString() + "3")); + timestamp_1_3_map.put(field3, gs(field3.toString() + "3")); + GlideString timestamp_1_3 = + client + .xadd(key1, timestamp_1_3_map, StreamAddOptionsBinary.builder().id(gs("1-3")).build()) + .get(); + assertNotNull(timestamp_1_3); + + GlideString timestamp_2_3 = + client + .xadd( + key2, + Map.of(field2, gs(field2.toString() + "3")), + StreamAddOptionsBinary.builder().id(gs("2-3")).build()) + .get(); + assertNotNull(timestamp_2_3); + + Map> result = + client.xreadBinary(Map.of(key1, timestamp_1_1, key2, timestamp_2_1)).get(); + + // check key1 + Map expected_key1 = new LinkedHashMap<>(); + expected_key1.put(timestamp_1_2, new GlideString[][] {{field1, gs(field1.toString() + "2")}}); + expected_key1.put( + timestamp_1_3, + new GlideString[][] { + {field1, gs(field1.toString() + "3")}, + {field3, gs(field3.toString() + "3")} + }); + assertDeepEquals(expected_key1, result.get(key1)); + + // check key2 + Map expected_key2 = new LinkedHashMap<>(); + expected_key2.put(timestamp_2_2, new GlideString[][] {{field2, gs(field2.toString() + "2")}}); + expected_key2.put(timestamp_2_3, new GlideString[][] {{field2, gs(field2.toString() + "3")}}); + assertDeepEquals(expected_key2, result.get(key2)); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xread_return_failures(BaseClient client) { + String key1 = "{key}:1" + UUID.randomUUID(); + String nonStreamKey = "{key}:3" + UUID.randomUUID(); + String field1 = "f1_"; + + // setup first entries in streams key1 and key2 + Map timestamp_1_1_map = new LinkedHashMap<>(); + timestamp_1_1_map.put(field1, field1 + "1"); + String timestamp_1_1 = + client.xadd(key1, timestamp_1_1_map, StreamAddOptions.builder().id("1-1").build()).get(); + assertNotNull(timestamp_1_1); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(nonStreamKey, "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xread(Map.of(nonStreamKey, timestamp_1_1, key1, timestamp_1_1)).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.xread(Map.of(key1, timestamp_1_1, nonStreamKey, timestamp_1_1)).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + long oneSecondInMS = 1000L; + assertNull( + testClient + .xread( + Map.of(key1, timestamp_1_1), + StreamReadOptions.builder().block(oneSecondInMS).build()) + .get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .xread(Map.of(key1, timestamp_1_1), StreamReadOptions.builder().block(0L).build()) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xread_binary_return_failures(BaseClient client) { + GlideString key1 = gs("{key}:1" + UUID.randomUUID()); + GlideString nonStreamKey = gs("{key}:3" + UUID.randomUUID()); + GlideString field1 = gs("f1_"); + + // setup first entries in streams key1 and key2 + Map timestamp_1_1_map = new LinkedHashMap<>(); + timestamp_1_1_map.put(field1, gs(field1.toString() + "1")); + GlideString timestamp_1_1 = + client + .xadd(key1, timestamp_1_1_map, StreamAddOptionsBinary.builder().id(gs("1-1")).build()) + .get(); + assertNotNull(timestamp_1_1); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(nonStreamKey, gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client.xreadBinary(Map.of(nonStreamKey, timestamp_1_1, key1, timestamp_1_1)).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client.xreadBinary(Map.of(key1, timestamp_1_1, nonStreamKey, timestamp_1_1)).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + long oneSecondInMS = 1000L; + assertNull( + testClient + .xreadBinary( + Map.of(key1, timestamp_1_1), + StreamReadOptions.builder().block(oneSecondInMS).build()) + .get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .xreadBinary( + Map.of(key1, timestamp_1_1), StreamReadOptions.builder().block(0L).build()) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xdel(BaseClient client) { + + String key = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String streamId1 = "0-1"; + String streamId2 = "0-2"; + String streamId3 = "0-3"; + + assertEquals( + streamId1, + client + .xadd( + key, + Map.of("f1", "foo1", "f2", "bar2"), + StreamAddOptions.builder().id(streamId1).build()) + .get()); + assertEquals( + streamId2, + client + .xadd( + key, + Map.of("f1", "foo1", "f2", "bar2"), + StreamAddOptions.builder().id(streamId2).build()) + .get()); + assertEquals(2L, client.xlen(key).get()); + + // Deletes one stream id, and ignores anything invalid: + assertEquals(1L, client.xdel(key, new String[] {streamId1, streamId3}).get()); + assertEquals(0L, client.xdel(key2, new String[] {streamId3}).get()); + + // Throw Exception: Key exists - but it is not a stream + assertEquals(OK, client.set(key2, "xdeltest").get()); + + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.xdel(key2, new String[] {streamId3}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xrange_and_xrevrange(BaseClient client) { + + String key = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String streamId1 = "0-1"; + String streamId2 = "0-2"; + String streamId3 = "0-3"; + + assertEquals( + streamId1, + client + .xadd( + key, + Map.of("f1", "foo1", "f2", "bar2"), + StreamAddOptions.builder().id(streamId1).build()) + .get()); + assertEquals( + streamId2, + client + .xadd( + key, + Map.of("f1", "foo1", "f2", "bar2"), + StreamAddOptions.builder().id(streamId2).build()) + .get()); + assertEquals(2L, client.xlen(key).get()); + + // get everything from the stream + Map result = client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(2, result.size()); + assertNotNull(result.get(streamId1)); + assertNotNull(result.get(streamId2)); + + // get everything from the stream using a reverse range search + Map revResult = + client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(2, revResult.size()); + assertNotNull(revResult.get(streamId1)); + assertNotNull(revResult.get(streamId2)); + + // returns empty if + before - + Map emptyResult = + client.xrange(key, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(0, emptyResult.size()); + + // rev search returns empty if - before + + Map emptyRevResult = + client.xrevrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(0, emptyRevResult.size()); + + assertEquals( + streamId3, + client + .xadd( + key, + Map.of("f3", "foo3", "f4", "bar3"), + StreamAddOptions.builder().id(streamId3).build()) + .get()); + + // get the newest entry + Map newResult = + client.xrange(key, IdBound.ofExclusive(streamId2), IdBound.ofExclusive(5), 1L).get(); + assertEquals(1, newResult.size()); + assertNotNull(newResult.get(streamId3)); + // ...and from xrevrange + Map newRevResult = + client.xrevrange(key, IdBound.ofExclusive(5), IdBound.ofExclusive(streamId2), 1L).get(); + assertEquals(1, newRevResult.size()); + assertNotNull(newRevResult.get(streamId3)); + + // xrange against an emptied stream + assertEquals(3, client.xdel(key, new String[] {streamId1, streamId2, streamId3}).get()); + Map emptiedResult = + client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX, 10L).get(); + assertEquals(0, emptiedResult.size()); + // ...and xrevrange + Map emptiedRevResult = + client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, emptiedRevResult.size()); + + // xrange against a non-existent stream + emptyResult = client.xrange(key2, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(0, emptyResult.size()); + // ...and xrevrange + emptiedRevResult = client.xrevrange(key2, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(0, emptiedRevResult.size()); + + // xrange against a non-stream value + assertEquals(OK, client.set(key2, "not_a_stream").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xrange(key2, InfRangeBound.MIN, InfRangeBound.MAX).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // ...and xrevrange + executionException = + assertThrows( + ExecutionException.class, + () -> client.xrevrange(key2, InfRangeBound.MAX, InfRangeBound.MIN).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // xrange when range bound is not valid ID + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrange(key, IdBound.ofExclusive("not_a_stream_id"), InfRangeBound.MAX) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrange(key, InfRangeBound.MIN, IdBound.ofExclusive("not_a_stream_id")) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // ... and xrevrange + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrevrange(key, IdBound.ofExclusive("not_a_stream_id"), InfRangeBound.MIN) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrevrange(key, InfRangeBound.MAX, IdBound.ofExclusive("not_a_stream_id")) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xrange_and_xrevrange_binary(BaseClient client) { + + GlideString key = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + String streamId1 = "0-1"; + String streamId2 = "0-2"; + String streamId3 = "0-3"; + + assertEquals( + gs(streamId1), + client + .xadd( + key, + Map.of(gs("f1"), gs("foo1"), gs("f2"), gs("bar2")), + StreamAddOptionsBinary.builder().id(gs(streamId1)).build()) + .get()); + assertEquals( + gs(streamId2), + client + .xadd( + key, + Map.of(gs("f1"), gs("foo1"), gs("f2"), gs("bar2")), + StreamAddOptionsBinary.builder().id(gs(streamId2)).build()) + .get()); + assertEquals(2L, client.xlen(key).get()); + + // get everything from the stream + Map result = + client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(2, result.size()); + assertNotNull(result.get(gs(streamId1))); + assertNotNull(result.get(gs(streamId2))); + + // get everything from the stream using a reverse range search + Map revResult = + client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(2, revResult.size()); + assertNotNull(revResult.get(gs(streamId1))); + assertNotNull(revResult.get(gs(streamId2))); + + // returns empty if + before - + Map emptyResult = + client.xrange(key, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(0, emptyResult.size()); + + // rev search returns empty if - before + + Map emptyRevResult = + client.xrevrange(key, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(0, emptyRevResult.size()); + + assertEquals( + gs(streamId3), + client + .xadd( + key, + Map.of(gs("f3"), gs("foo3"), gs("f4"), gs("bar3")), + StreamAddOptionsBinary.builder().id(gs(streamId3)).build()) + .get()); + + // get the newest entry + Map newResult = + client.xrange(key, IdBound.ofExclusive(streamId2), IdBound.ofExclusive(5), 1L).get(); + assertEquals(1, newResult.size()); + assertNotNull(newResult.get(gs(streamId3))); + // ...and from xrevrange + Map newRevResult = + client.xrevrange(key, IdBound.ofExclusive(5), IdBound.ofExclusive(streamId2), 1L).get(); + assertEquals(1, newRevResult.size()); + assertNotNull(newRevResult.get(gs(streamId3))); + + // xrange against an emptied stream + assertEquals( + 3, client.xdel(key, new GlideString[] {gs(streamId1), gs(streamId2), gs(streamId3)}).get()); + Map emptiedResult = + client.xrange(key, InfRangeBound.MIN, InfRangeBound.MAX, 10L).get(); + assertEquals(0, emptiedResult.size()); + // ...and xrevrange + Map emptiedRevResult = + client.xrevrange(key, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, emptiedRevResult.size()); + + // xrange against a non-existent stream + emptyResult = client.xrange(key2, InfRangeBound.MIN, InfRangeBound.MAX).get(); + assertEquals(0, emptyResult.size()); + // ...and xrevrange + emptiedRevResult = client.xrevrange(key2, InfRangeBound.MAX, InfRangeBound.MIN).get(); + assertEquals(0, emptiedRevResult.size()); + + // xrange against a non-stream value + assertEquals(OK, client.set(key2, gs("not_a_stream")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xrange(key2, InfRangeBound.MIN, InfRangeBound.MAX).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // ...and xrevrange + executionException = + assertThrows( + ExecutionException.class, + () -> client.xrevrange(key2, InfRangeBound.MAX, InfRangeBound.MIN).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // xrange when range bound is not valid ID + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrange(key, IdBound.ofExclusive("not_a_stream_id"), InfRangeBound.MAX) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrange(key, InfRangeBound.MIN, IdBound.ofExclusive("not_a_stream_id")) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // ... and xrevrange + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrevrange(key, IdBound.ofExclusive("not_a_stream_id"), InfRangeBound.MIN) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xrevrange(key, InfRangeBound.MAX, IdBound.ofExclusive("not_a_stream_id")) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xgroupCreate_xgroupDestroy(BaseClient client) { + String key = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String groupName = "group" + UUID.randomUUID(); + String streamId = "0-1"; + + // Stream not created results in error + Exception executionException = + assertThrows( + ExecutionException.class, () -> client.xgroupCreate(key, groupName, streamId).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Stream with option to create creates stream & Group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, streamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + + // ...and again results in BUSYGROUP error, because group names must be unique + executionException = + assertThrows( + ExecutionException.class, () -> client.xgroupCreate(key, groupName, streamId).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("BUSYGROUP")); + + // Stream Group can be destroyed returns: true + assertEquals(true, client.xgroupDestroy(key, groupName).get()); + + // ...and again results in: false + assertEquals(false, client.xgroupDestroy(key, groupName).get()); + + // ENTRIESREAD option was added in valkey 7.0.0 + StreamGroupOptions entriesReadOption = StreamGroupOptions.builder().entriesRead(10L).build(); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(OK, client.xgroupCreate(key, groupName, streamId, entriesReadOption).get()); + } else { + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupCreate(key, groupName, streamId, entriesReadOption).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + // key is a string and cannot be created as a stream + assertEquals(OK, client.set(stringKey, "not_a_stream").get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xgroupCreate( + stringKey, + groupName, + streamId, + StreamGroupOptions.builder().makeStream().build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.xgroupDestroy(stringKey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xgroupCreate_binary_xgroupDestroy_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString groupName = gs("group" + UUID.randomUUID()); + GlideString streamId = gs("0-1"); + + // Stream not created results in error + Exception executionException = + assertThrows( + ExecutionException.class, () -> client.xgroupCreate(key, groupName, streamId).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Stream with option to create creates stream & Group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, streamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + + // ...and again results in BUSYGROUP error, because group names must be unique + executionException = + assertThrows( + ExecutionException.class, () -> client.xgroupCreate(key, groupName, streamId).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("BUSYGROUP")); + + // Stream Group can be destroyed returns: true + assertEquals(true, client.xgroupDestroy(key, groupName).get()); + + // ...and again results in: false + assertEquals(false, client.xgroupDestroy(key, groupName).get()); + + // ENTRIESREAD option was added in valkey 7.0.0 + StreamGroupOptions entriesReadOption = StreamGroupOptions.builder().entriesRead(10L).build(); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(OK, client.xgroupCreate(key, groupName, streamId, entriesReadOption).get()); + } else { + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupCreate(key, groupName, streamId, entriesReadOption).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + // key is a string and cannot be created as a stream + assertEquals(OK, client.set(stringKey, gs("not_a_stream")).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xgroupCreate( + stringKey, + groupName, + streamId, + StreamGroupOptions.builder().makeStream().build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.xgroupDestroy(stringKey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xinfoGroups_with_xinfoConsumers(BaseClient client) { + String key = UUID.randomUUID().toString(); + String groupName1 = "group1" + UUID.randomUUID(); + String groupName2 = "group2" + UUID.randomUUID(); + String consumer1 = "consumer1" + UUID.randomUUID(); + String consumer2 = "consumer2" + UUID.randomUUID(); + String streamId0_0 = "0-0"; + String streamId1_0 = "1-0"; + String streamId1_1 = "1-1"; + String streamId1_2 = "1-2"; + String streamId1_3 = "1-3"; + LinkedHashMap streamMap = new LinkedHashMap(); + streamMap.put("f1", "v1"); + streamMap.put("f2", "v2"); + + assertEquals( + streamId1_0, + client.xadd(key, streamMap, StreamAddOptions.builder().id(streamId1_0).build()).get()); + assertEquals( + streamId1_1, + client + .xadd(key, Map.of("f3", "v3"), StreamAddOptions.builder().id(streamId1_1).build()) + .get()); + assertEquals( + streamId1_2, + client + .xadd(key, Map.of("f4", "v4"), StreamAddOptions.builder().id(streamId1_2).build()) + .get()); + assertEquals(OK, client.xgroupCreate(key, groupName1, streamId0_0).get()); + + Map> result = + client + .xreadgroup( + Map.of(key, ">"), + groupName1, + consumer1, + StreamReadGroupOptions.builder().count(1L).build()) + .get(); + assertDeepEquals( + Map.of(key, Map.of(streamId1_0, new String[][] {{"f1", "v1"}, {"f2", "v2"}})), result); + + // Sleep to ensure the idle time value and inactive time value returned by xinfo_consumers is >0 + Thread.sleep(2000); + Map[] consumers = client.xinfoConsumers(key, groupName1).get(); + assertEquals(1, consumers.length); + Map consumerInfo = consumers[0]; + assertEquals(consumer1, consumerInfo.get("name")); + assertEquals(1L, consumerInfo.get("pending")); + assertTrue((Long) consumerInfo.get("idle") > 0L); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")) { + assertTrue((Long) consumerInfo.get("inactive") > 0L); + } + + // Test with GlideString + Map[] binaryConsumers = + client.xinfoConsumers(gs(key), gs(groupName1)).get(); + assertEquals(1, binaryConsumers.length); + Map binaryConsumerInfo = binaryConsumers[0]; + assertEquals(gs(consumer1), binaryConsumerInfo.get(gs("name"))); + assertEquals(1L, binaryConsumerInfo.get(gs("pending"))); + assertTrue((Long) binaryConsumerInfo.get(gs("idle")) > 0L); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")) { + assertTrue((Long) binaryConsumerInfo.get(gs("inactive")) > 0L); + } + + // Create consumer2 and read the rest of the entries with it + assertTrue(client.xgroupCreateConsumer(key, groupName1, consumer2).get()); + result = client.xreadgroup(Map.of(key, ">"), groupName1, consumer2).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamId1_1, new String[][] {{"f3", "v3"}}, + streamId1_2, new String[][] {{"f4", "v4"}})), + result); + + // Verify that xinfo_consumers contains info for 2 consumers now + // Test with byte string args + consumers = client.xinfoConsumers(key, groupName1).get(); + assertEquals(2, consumers.length); + + // Add one more entry + assertEquals( + streamId1_3, + client + .xadd(key, Map.of("f5", "v5"), StreamAddOptions.builder().id(streamId1_3).build()) + .get()); + + Map[] groups = client.xinfoGroups(key).get(); + assertEquals(1, groups.length); + Map group1Info = groups[0]; + assertEquals(groupName1, group1Info.get("name")); + assertEquals(2L, group1Info.get("consumers")); + assertEquals(3L, group1Info.get("pending")); + assertEquals(streamId1_2, group1Info.get("last-delivered-id")); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals( + 3L, group1Info.get("entries-read")); // We have read stream entries 1-0, 1-1, and 1-2 + assertEquals( + 1L, group1Info.get("lag")); // We still have not read one entry in the stream, entry 1-3 + } + + // Test with GlideString + Map[] binaryGroups = client.xinfoGroups(gs(key)).get(); + assertEquals(1, binaryGroups.length); + Map binaryGroup1Info = binaryGroups[0]; + assertEquals(gs(groupName1), binaryGroup1Info.get(gs("name"))); + assertEquals(2L, binaryGroup1Info.get(gs("consumers"))); + assertEquals(3L, binaryGroup1Info.get(gs("pending"))); + assertEquals(gs(streamId1_2), binaryGroup1Info.get(gs("last-delivered-id"))); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals( + 3L, + binaryGroup1Info.get( + gs("entries-read"))); // We have read stream entries 1-0, 1-1, and 1-2 + assertEquals( + 1L, + binaryGroup1Info.get( + gs("lag"))); // We still have not read one entry in the stream, entry 1-3 + } + + // Verify xgroup_set_id effects the returned value from xinfo_groups + assertEquals(OK, client.xgroupSetId(key, groupName1, streamId1_1).get()); + groups = client.xinfoGroups(key).get(); + assertEquals(1, groups.length); + group1Info = groups[0]; + assertEquals(groupName1, group1Info.get("name")); + assertEquals(2L, group1Info.get("consumers")); + assertEquals(3L, group1Info.get("pending")); + assertEquals(streamId1_1, group1Info.get("last-delivered-id")); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertNull( + group1Info.get("entries-read")); // Gets set to None when we change the last delivered ID + assertNull(group1Info.get("lag")); // Gets set to None when we change the last delivered ID + + // Verify xgroup_set_id with entries_read_id effects the returned value from xinfo_groups + assertEquals(OK, client.xgroupSetId(key, groupName1, streamId1_1, 1L).get()); + groups = client.xinfoGroups(key).get(); + assertEquals(1, groups.length); + group1Info = groups[0]; + assertEquals(groupName1, group1Info.get("name")); + assertEquals(2L, group1Info.get("consumers")); + assertEquals(3L, group1Info.get("pending")); + assertEquals(streamId1_1, group1Info.get("last-delivered-id")); + assertEquals(1L, group1Info.get("entries-read")); + assertEquals(3L, group1Info.get("lag")); + } + + // Add one more consumer group + assertEquals(OK, client.xgroupCreate(key, groupName2, streamId0_0).get()); + + // Verify that xinfo_groups contains info for 2 consumer groups now + groups = client.xinfoGroups(key).get(); + assertEquals(2, groups.length); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xinfoGroups_with_xinfoConsumers_and_edge_cases(BaseClient client) { + String key = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String nonExistentKey = UUID.randomUUID().toString(); + String groupName = "group1" + UUID.randomUUID(); + String streamId1_0 = "1-0"; + + // Passing a non-existing key raises an error + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.xinfoGroups(nonExistentKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.xinfoConsumers(nonExistentKey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + assertEquals( + streamId1_0, + client + .xadd( + key, + Map.of("f1", "v1", "f2", "v2"), + StreamAddOptions.builder().id(streamId1_0).build()) + .get()); + + // Passing a non-existing group raises an error + executionException = + assertThrows( + ExecutionException.class, () -> client.xinfoConsumers(key, "nonExistentGroup").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // No groups exist yet + assertEquals(0, client.xinfoGroups(key).get().length); + + assertEquals(OK, client.xgroupCreate(key, groupName, streamId1_0).get()); + + // No consumers exist yet + assertEquals(0, client.xinfoConsumers(key, groupName).get().length); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(stringKey, "foo").get()); + executionException = + assertThrows(ExecutionException.class, () -> client.xinfoGroups(stringKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.xinfoConsumers(stringKey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xgroupCreateConsumer_xgroupDelConsumer_xreadgroup_xack(BaseClient client) { + String key = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String groupName = "group" + UUID.randomUUID(); + String zeroStreamId = "0"; + String consumerName = "consumer" + UUID.randomUUID(); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // create consumer for group that does not exist results in a NOGROUP request error + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupCreateConsumer(key, "not_a_group", consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // create consumer for group again + assertFalse(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // Deletes a consumer that is not created yet returns 0 + assertEquals(0L, client.xgroupDelConsumer(key, groupName, "not_a_consumer").get()); + + // Add two stream entries + String streamid_1 = client.xadd(key, Map.of("field1", "value1")).get(); + assertNotNull(streamid_1); + String streamid_2 = client.xadd(key, Map.of("field2", "value2")).get(); + assertNotNull(streamid_2); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = client.xreadgroup(Map.of(key, ">"), groupName, consumerName).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_1, new String[][] {{"field1", "value1"}}, + streamid_2, new String[][] {{"field2", "value2"}})), + result_1); + + // delete one of the streams + assertEquals(1L, client.xdel(key, new String[] {streamid_1}).get()); + + // now xreadgroup returns one empty stream and one non-empty stream + var result_2 = client.xreadgroup(Map.of(key, "0"), groupName, consumerName).get(); + assertEquals(2, result_2.get(key).size()); + assertNull(result_2.get(key).get(streamid_1)); + assertArrayEquals(new String[][] {{"field2", "value2"}}, result_2.get(key).get(streamid_2)); + + String streamid_3 = client.xadd(key, Map.of("field3", "value3")).get(); + assertNotNull(streamid_3); + + // xack that streamid_1, and streamid_2 was received + assertEquals(2L, client.xack(key, groupName, new String[] {streamid_1, streamid_2}).get()); + + // Delete the consumer group and expect 1 pending messages (one was received) + assertEquals(0L, client.xgroupDelConsumer(key, groupName, consumerName).get()); + + // xack streamid_1, and streamid_2 already received returns 0L + assertEquals(0L, client.xack(key, groupName, new String[] {streamid_1, streamid_2}).get()); + + // Consume the last message with the previously deleted consumer (creates the consumer anew) + var result_3 = client.xreadgroup(Map.of(key, ">"), groupName, consumerName).get(); + assertEquals(1, result_3.get(key).size()); + + // wrong group, so xack streamid_3 returns 0 + assertEquals(0L, client.xack(key, "not_a_group", new String[] {streamid_3}).get()); + + // Delete the consumer group and expect the pending message + assertEquals(1L, client.xgroupDelConsumer(key, groupName, consumerName).get()); + + // key is a string and cannot be created as a stream + assertEquals(OK, client.set(stringKey, "not_a_stream").get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupCreateConsumer(stringKey, groupName, consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupDelConsumer(stringKey, groupName, consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xgroupCreateConsumer_xgroupDelConsumer_xreadgroup_xack_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString groupName = gs("group" + UUID.randomUUID()); + GlideString zeroStreamId = gs("0"); + GlideString consumerName = gs("consumer" + UUID.randomUUID()); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // create consumer for group that does not exist results in a NOGROUP request error + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupCreateConsumer(key, gs("not_a_group"), consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // create consumer for group again + assertFalse(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // Deletes a consumer that is not created yet returns 0 + assertEquals(0L, client.xgroupDelConsumer(key, groupName, gs("not_a_consumer")).get()); + + // Add two stream entries + GlideString streamid_1 = client.xadd(key, Map.of(gs("field1"), gs("value1"))).get(); + assertNotNull(streamid_1); + GlideString streamid_2 = client.xadd(key, Map.of(gs("field2"), gs("value2"))).get(); + assertNotNull(streamid_2); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = client.xreadgroup(Map.of(key, gs(">")), groupName, consumerName).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_1, new GlideString[][] {{gs("field1"), gs("value1")}}, + streamid_2, new GlideString[][] {{gs("field2"), gs("value2")}})), + result_1); + + // delete one of the streams + assertEquals(1L, client.xdel(key, new GlideString[] {streamid_1}).get()); + + // now xreadgroup returns one empty stream and one non-empty stream + var result_2 = client.xreadgroup(Map.of(key, gs("0")), groupName, consumerName).get(); + assertEquals(2, result_2.get(key).size()); + assertNull(result_2.get(key).get(streamid_1)); + assertArrayEquals( + new GlideString[][] {{gs("field2"), gs("value2")}}, result_2.get(key).get(streamid_2)); + + GlideString streamid_3 = client.xadd(key, Map.of(gs("field3"), gs("value3"))).get(); + assertNotNull(streamid_3); + + // xack that streamid_1, and streamid_2 was received + assertEquals(2L, client.xack(key, groupName, new GlideString[] {streamid_1, streamid_2}).get()); + + // Delete the consumer group and expect 1 pending messages (one was received) + assertEquals(0L, client.xgroupDelConsumer(key, groupName, consumerName).get()); + + // xack streamid_1, and streamid_2 already received returns 0L + assertEquals(0L, client.xack(key, groupName, new GlideString[] {streamid_1, streamid_2}).get()); + + // Consume the last message with the previously deleted consumer (creates the consumer anew) + var result_3 = client.xreadgroup(Map.of(key, gs(">")), groupName, consumerName).get(); + assertEquals(1, result_3.get(key).size()); + + // wrong group, so xack streamid_3 returns 0 + assertEquals(0L, client.xack(key, gs("not_a_group"), new GlideString[] {streamid_3}).get()); + + // Delete the consumer group and expect the pending message + assertEquals(1L, client.xgroupDelConsumer(key, groupName, consumerName).get()); + + // key is a string and cannot be created as a stream + assertEquals(OK, client.set(stringKey, gs("not_a_stream")).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupCreateConsumer(stringKey, groupName, consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupDelConsumer(stringKey, groupName, consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xgroupSetId_entriesRead(BaseClient client) { + String key = "testKey" + UUID.randomUUID(); + String nonExistingKey = "group" + UUID.randomUUID(); + String stringKey = "testKey" + UUID.randomUUID(); + String groupName = UUID.randomUUID().toString(); + String consumerName = UUID.randomUUID().toString(); + String streamId0 = "0"; + String streamId1_0 = "1-0"; + String streamId1_1 = "1-1"; + String streamId1_2 = "1-2"; + + // Setup: Create stream with 3 entries, create consumer group, read entries to add them to the + // Pending Entries List. + assertEquals( + streamId1_0, + client + .xadd(key, Map.of("f0", "v0"), StreamAddOptions.builder().id(streamId1_0).build()) + .get()); + assertEquals( + streamId1_1, + client + .xadd(key, Map.of("f1", "v1"), StreamAddOptions.builder().id(streamId1_1).build()) + .get()); + assertEquals( + streamId1_2, + client + .xadd(key, Map.of("f2", "v2"), StreamAddOptions.builder().id(streamId1_2).build()) + .get()); + + assertEquals(OK, client.xgroupCreate(key, groupName, streamId0).get()); + + var result = client.xreadgroup(Map.of(key, ">"), groupName, consumerName).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamId1_0, new String[][] {{"f0", "v0"}}, + streamId1_1, new String[][] {{"f1", "v1"}}, + streamId1_2, new String[][] {{"f2", "v2"}})), + result); + + // Sanity check: xreadgroup should not return more entries since they're all already in the + // Pending Entries List. + assertNull(client.xreadgroup(Map.of(key, ">"), groupName, consumerName).get()); + + // Reset the last delivered ID for the consumer group to "1-1". + // ENTRIESREAD is only supported in Valkey version 7.0.0 and higher. + if (SERVER_VERSION.isLowerThan("7.0.0")) { + assertEquals(OK, client.xgroupSetId(key, groupName, streamId1_1).get()); + } else { + assertEquals(OK, client.xgroupSetId(key, groupName, streamId1_1, 1L).get()); + } + + // xreadgroup should only return entry 1-2 since we reset the last delivered ID to 1-1. + result = client.xreadgroup(Map.of(key, ">"), groupName, consumerName).get(); + assertDeepEquals(Map.of(key, Map.of(streamId1_2, new String[][] {{"f2", "v2"}})), result); + + // An error is raised if XGROUP SETID is called with a non-existing key. + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupSetId(nonExistingKey, groupName, streamId0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // An error is raised if XGROUP SETID is called with a non-existing group. + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupSetId(key, "non_existing_group", streamId0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Setting the ID to a non-existing ID is allowed + assertEquals("OK", client.xgroupSetId(key, groupName, "99-99").get()); + + // Key exists, but it is not a stream + assertEquals("OK", client.set(stringKey, "foo").get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupSetId(stringKey, groupName, streamId0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmemberBinaryWithCountWithScores(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + Map membersScores = Map.of(gs("one"), 1.0, gs("two"), 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + // Unique values are expected as count is positive + Object[][] randMembersWithScores = client.zrandmemberWithCountWithScores(key1, 4).get(); + assertEquals(2, randMembersWithScores.length); + for (Object[] membersWithScore : randMembersWithScores) { + GlideString member = (GlideString) membersWithScore[0]; + Double score = (Double) membersWithScore[1]; + + assertEquals(score, membersScores.get(member)); + } + + // Duplicate values are expected as count is negative + randMembersWithScores = client.zrandmemberWithCountWithScores(key1, -4).get(); + assertEquals(4, randMembersWithScores.length); + for (Object[] randMembersWithScore : randMembersWithScores) { + GlideString member = (GlideString) randMembersWithScore[0]; + Double score = (Double) randMembersWithScore[1]; + + assertEquals(score, membersScores.get(member)); + } + + assertEquals(0, client.zrandmemberWithCountWithScores(key1, 0).get().length); + assertEquals(0, client.zrandmemberWithCountWithScores(gs("nonExistentKey"), 4).get().length); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.zrandmemberWithCountWithScores(key2, 5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xgroupSetId_entriesRead_binary(BaseClient client) { + GlideString key = gs("testKey" + UUID.randomUUID()); + GlideString nonExistingKey = gs("group" + UUID.randomUUID()); + GlideString stringKey = gs("testKey" + UUID.randomUUID()); + GlideString groupName = gs(UUID.randomUUID().toString()); + GlideString consumerName = gs(UUID.randomUUID().toString()); + GlideString streamId0 = gs("0"); + GlideString streamId1_0 = gs("1-0"); + GlideString streamId1_1 = gs("1-1"); + GlideString streamId1_2 = gs("1-2"); + + // Setup: Create stream with 3 entries, create consumer group, read entries to add them to the + // Pending Entries List. + assertEquals( + streamId1_0, + client + .xadd( + key, + Map.of(gs("f0"), gs("v0")), + StreamAddOptionsBinary.builder().id(streamId1_0).build()) + .get()); + assertEquals( + streamId1_1, + client + .xadd( + key, + Map.of(gs("f1"), gs("v1")), + StreamAddOptionsBinary.builder().id(streamId1_1).build()) + .get()); + assertEquals( + streamId1_2, + client + .xadd( + key, + Map.of(gs("f2"), gs("v2")), + StreamAddOptionsBinary.builder().id(streamId1_2).build()) + .get()); + + assertEquals(OK, client.xgroupCreate(key, groupName, streamId0).get()); + + var result = client.xreadgroup(Map.of(key, gs(">")), groupName, consumerName).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamId1_0, new GlideString[][] {{gs("f0"), gs("v0")}}, + streamId1_1, new GlideString[][] {{gs("f1"), gs("v1")}}, + streamId1_2, new GlideString[][] {{gs("f2"), gs("v2")}})), + result); + + // Sanity check: xreadgroup should not return more entries since they're all already in the + // Pending Entries List. + assertNull(client.xreadgroup(Map.of(key, gs(">")), groupName, consumerName).get()); + + // Reset the last delivered ID for the consumer group to "1-1". + // ENTRIESREAD is only supported in Valkey version 7.0.0 and higher. + if (SERVER_VERSION.isLowerThan("7.0.0")) { + assertEquals(OK, client.xgroupSetId(key, groupName, streamId1_1).get()); + } else { + assertEquals(OK, client.xgroupSetId(key, groupName, streamId1_1, 1L).get()); + } + + // xreadgroup should only return entry 1-2 since we reset the last delivered ID to 1-1. + result = client.xreadgroup(Map.of(key, gs(">")), groupName, consumerName).get(); + assertDeepEquals( + Map.of(key, Map.of(streamId1_2, new GlideString[][] {{gs("f2"), gs("v2")}})), result); + + // An error is raised if XGROUP SETID is called with a non-existing key. + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupSetId(nonExistingKey, groupName, streamId0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // An error is raised if XGROUP SETID is called with a non-existing group. + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupSetId(key, gs("non_existing_group"), streamId0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Setting the ID to a non-existing ID is allowed + assertEquals("OK", client.xgroupSetId(key, groupName, gs("99-99")).get()); + + // Key exists, but it is not a stream + assertEquals("OK", client.set(stringKey, gs("foo")).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.xgroupSetId(stringKey, groupName, streamId0).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmemberBinaryWithCount(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + Map membersScores = Map.of(gs("one"), 1.0, gs("two"), 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + // Unique values are expected as count is positive + List randMembers = Arrays.asList(client.zrandmemberWithCount(key1, 4).get()); + assertEquals(2, randMembers.size()); + assertEquals(2, new HashSet<>(randMembers).size()); + randMembers.forEach(member -> assertTrue(membersScores.containsKey(member))); + + // Duplicate values are expected as count is negative + randMembers = Arrays.asList(client.zrandmemberWithCount(key1, -4).get()); + assertEquals(4, randMembers.size()); + randMembers.forEach(member -> assertTrue(membersScores.containsKey(member))); + + assertEquals(0, client.zrandmemberWithCount(key1, 0).get().length); + assertEquals(0, client.zrandmemberWithCount(gs("nonExistentKey"), 4).get().length); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("bar")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrandmemberWithCount(key2, 5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xreadgroup_return_failures(BaseClient client) { + String key = "{key}:1" + UUID.randomUUID(); + String nonStreamKey = "{key}:3" + UUID.randomUUID(); + String groupName = "group" + UUID.randomUUID(); + String zeroStreamId = "0"; + String consumerName = "consumer" + UUID.randomUUID(); + + // setup first entries in streams key1 and key2 + String timestamp_1_1 = + client.xadd(key, Map.of("f1", "v1"), StreamAddOptions.builder().id("1-1").build()).get(); + assertNotNull(timestamp_1_1); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // First key exists, but it is not a stream + assertEquals(OK, client.set(nonStreamKey, "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xreadgroup( + Map.of(nonStreamKey, timestamp_1_1, key, timestamp_1_1), + groupName, + consumerName) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Second key exists, but it is not a stream + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xreadgroup( + Map.of(key, timestamp_1_1, nonStreamKey, timestamp_1_1), + groupName, + consumerName) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // group doesn't exists, throws a request error with "NOGROUP" + executionException = + assertThrows( + ExecutionException.class, + () -> client.xreadgroup(Map.of(key, timestamp_1_1), "not_a_group", consumerName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // consumer doesn't exist and will be created + var emptyResult = + client.xreadgroup(Map.of(key, timestamp_1_1), groupName, "non_existing_consumer").get(); + // no available pending messages + assertEquals(0, emptyResult.get(key).size()); + + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + String timeoutKey = "{key}:2" + UUID.randomUUID(); + String timeoutGroupName = "group" + UUID.randomUUID(); + String timeoutConsumerName = "consumer" + UUID.randomUUID(); + + // Create a group read with the test client + // add a single stream entry and consumer + // the first call to ">" will return an update consumer group + // the second call to ">" will block waiting for new entries + // using anything other than ">" won't block, but will return the empty consumer result + // see: https://github.com/redis/redis/issues/6587 + assertEquals( + OK, + testClient + .xgroupCreate( + timeoutKey, + timeoutGroupName, + zeroStreamId, + StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue( + testClient.xgroupCreateConsumer(timeoutKey, timeoutGroupName, timeoutConsumerName).get()); + String streamid_1 = testClient.xadd(timeoutKey, Map.of("field1", "value1")).get(); + assertNotNull(streamid_1); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = + testClient + .xreadgroup(Map.of(timeoutKey, ">"), timeoutGroupName, timeoutConsumerName) + .get(); + // returns a null result on the key + assertNull(result_1.get(key)); + + // subsequent calls to read ">" will block: + // ensure that command doesn't time out even if timeout > request timeout + long oneSecondInMS = 1000L; + assertNull( + testClient + .xreadgroup( + Map.of(timeoutKey, ">"), + timeoutGroupName, + timeoutConsumerName, + StreamReadGroupOptions.builder().block(oneSecondInMS).build()) + .get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .xreadgroup( + Map.of(timeoutKey, ">"), + timeoutGroupName, + timeoutConsumerName, + StreamReadGroupOptions.builder().block(0L).build()) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xreadgroup_binary_return_failures(BaseClient client) { + GlideString key = gs("{key}:1" + UUID.randomUUID()); + GlideString nonStreamKey = gs("{key}:3" + UUID.randomUUID()); + GlideString groupName = gs("group" + UUID.randomUUID()); + GlideString zeroStreamId = gs("0"); + GlideString consumerName = gs("consumer" + UUID.randomUUID()); + + // setup first entries in streams key1 and key2 + GlideString timestamp_1_1 = + client + .xadd( + key, + Map.of(gs("f1"), gs("v1")), + StreamAddOptionsBinary.builder().id(gs("1-1")).build()) + .get(); + assertNotNull(timestamp_1_1); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // First key exists, but it is not a stream + assertEquals(OK, client.set(nonStreamKey, gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xreadgroup( + Map.of(nonStreamKey, timestamp_1_1, key, timestamp_1_1), + groupName, + consumerName) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Second key exists, but it is not a stream + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xreadgroup( + Map.of(key, timestamp_1_1, nonStreamKey, timestamp_1_1), + groupName, + consumerName) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // group doesn't exists, throws a request error with "NOGROUP" + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xreadgroup(Map.of(key, timestamp_1_1), gs("not_a_group"), consumerName) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // consumer doesn't exist and will be created + var emptyResult = + client.xreadgroup(Map.of(key, timestamp_1_1), groupName, gs("non_existing_consumer")).get(); + // no available pending messages + assertEquals(0, emptyResult.get(key).size()); + + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + GlideString timeoutKey = gs("{key}:2" + UUID.randomUUID()); + GlideString timeoutGroupName = gs("group" + UUID.randomUUID()); + GlideString timeoutConsumerName = gs("consumer" + UUID.randomUUID()); + + // Create a group read with the test client + // add a single stream entry and consumer + // the first call to gs(">") will return an update consumer group + // the second call to gs(">") will block waiting for new entries + // using anything other than gs(">") won't block, but will return the empty consumer result + // see: https://github.com/redis/redis/issues/6587 + assertEquals( + OK, + testClient + .xgroupCreate( + timeoutKey, + timeoutGroupName, + zeroStreamId, + StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue( + testClient.xgroupCreateConsumer(timeoutKey, timeoutGroupName, timeoutConsumerName).get()); + GlideString streamid_1 = + testClient.xadd(timeoutKey, Map.of(gs("field1"), gs("value1"))).get(); + assertNotNull(streamid_1); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = + testClient + .xreadgroup(Map.of(timeoutKey, gs(">")), timeoutGroupName, timeoutConsumerName) + .get(); + // returns a null result on the key + assertNull(result_1.get(key)); + + // subsequent calls to read ">" will block: + // ensure that command doesn't time out even if timeout > request timeout + long oneSecondInMS = 1000L; + assertNull( + testClient + .xreadgroup( + Map.of(timeoutKey, gs(">")), + timeoutGroupName, + timeoutConsumerName, + StreamReadGroupOptions.builder().block(oneSecondInMS).build()) + .get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .xreadgroup( + Map.of(timeoutKey, gs(">")), + timeoutGroupName, + timeoutConsumerName, + StreamReadGroupOptions.builder().block(0L).build()) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xack_return_failures(BaseClient client) { + String key = "{key}:1" + UUID.randomUUID(); + String nonStreamKey = "{key}:3" + UUID.randomUUID(); + String groupName = "group" + UUID.randomUUID(); + String zeroStreamId = "0"; + String consumerName = "consumer" + UUID.randomUUID(); + + // setup first entries in streams key1 and key2 + String timestamp_1_1 = + client.xadd(key, Map.of("f1", "v1"), StreamAddOptions.builder().id("1-1").build()).get(); + assertNotNull(timestamp_1_1); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumerName).get()); + + // Empty entity id list throws a RequestException + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.xack(key, groupName, new String[0]).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(nonStreamKey, "bar").get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.xack(nonStreamKey, groupName, new String[] {zeroStreamId}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmember_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + Map membersScores = Map.of(gs("one"), 1.0, gs("two"), 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + GlideString randMember = client.zrandmember(key1).get(); + assertTrue(membersScores.containsKey(randMember)); + assertNull(client.zrandmember(gs("nonExistentKey")).get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, gs("bar")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrandmember(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xpending_xclaim(BaseClient client) { + + String key = UUID.randomUUID().toString(); + String groupName = "group" + UUID.randomUUID(); + String zeroStreamId = "0"; + String consumer1 = "consumer-1-" + UUID.randomUUID(); + String consumer2 = "consumer-2-" + UUID.randomUUID(); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer1).get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer2).get()); + + // Add two stream entries for consumer 1 + String streamid_1 = client.xadd(key, Map.of("field1", "value1")).get(); + assertNotNull(streamid_1); + String streamid_2 = client.xadd(key, Map.of("field2", "value2")).get(); + assertNotNull(streamid_2); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = client.xreadgroup(Map.of(key, ">"), groupName, consumer1).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_1, new String[][] {{"field1", "value1"}}, + streamid_2, new String[][] {{"field2", "value2"}})), + result_1); + + // Add three stream entries for consumer 2 + String streamid_3 = client.xadd(key, Map.of("field3", "value3")).get(); + assertNotNull(streamid_3); + String streamid_4 = client.xadd(key, Map.of("field4", "value4")).get(); + assertNotNull(streamid_4); + String streamid_5 = client.xadd(key, Map.of("field5", "value5")).get(); + assertNotNull(streamid_5); + + // read the entire stream for the consumer and mark messages as pending + var result_2 = client.xreadgroup(Map.of(key, ">"), groupName, consumer2).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_3, new String[][] {{"field3", "value3"}}, + streamid_4, new String[][] {{"field4", "value4"}}, + streamid_5, new String[][] {{"field5", "value5"}})), + result_2); + + Object[] pending_results = client.xpending(key, groupName).get(); + Object[] expectedResult = { + Long.valueOf(5L), streamid_1, streamid_5, new Object[][] {{consumer1, "2"}, {consumer2, "3"}} + }; + assertDeepEquals(expectedResult, pending_results); + + // ensure idle_time > 0 + Thread.sleep(2000); + Object[][] pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L).get(); + + // because of idle time return, we have to remove it from the expected results + // and check it separately + assertArrayEquals( + new Object[] {streamid_1, consumer1, 1L}, + ArrayUtils.remove(pending_results_extended[0], 2)); + assertTrue((Long) pending_results_extended[0][2] > 0L); + + assertArrayEquals( + new Object[] {streamid_2, consumer1, 1L}, + ArrayUtils.remove(pending_results_extended[1], 2)); + assertTrue((Long) pending_results_extended[1][2] > 0L); + + assertArrayEquals( + new Object[] {streamid_3, consumer2, 1L}, + ArrayUtils.remove(pending_results_extended[2], 2)); + assertTrue((Long) pending_results_extended[2][2] >= 0L); + + assertArrayEquals( + new Object[] {streamid_4, consumer2, 1L}, + ArrayUtils.remove(pending_results_extended[3], 2)); + assertTrue((Long) pending_results_extended[3][2] >= 0L); + + assertArrayEquals( + new Object[] {streamid_5, consumer2, 1L}, + ArrayUtils.remove(pending_results_extended[4], 2)); + assertTrue((Long) pending_results_extended[4][2] >= 0L); + + // use claim to claim stream 3 and 5 for consumer 1 + var claimResults = + client.xclaim(key, groupName, consumer1, 0L, new String[] {streamid_3, streamid_5}).get(); + assertDeepEquals( + Map.of( + streamid_3, + new String[][] {{"field3", "value3"}}, + streamid_5, + new String[][] {{"field5", "value5"}}), + claimResults); + + var claimResultsJustId = + client + .xclaimJustId(key, groupName, consumer1, 0L, new String[] {streamid_3, streamid_5}) + .get(); + assertArrayEquals(new String[] {streamid_3, streamid_5}, claimResultsJustId); + + // add one more stream + String streamid_6 = client.xadd(key, Map.of("field6", "value6")).get(); + assertNotNull(streamid_6); + + // using force, we can xclaim the message without reading it + var claimForceResults = + client + .xclaim( + key, + groupName, + consumer2, + 0L, + new String[] {streamid_6}, + StreamClaimOptions.builder().force().retryCount(99L).build()) + .get(); + assertDeepEquals(Map.of(streamid_6, new String[][] {{"field6", "value6"}}), claimForceResults); + + Object[][] forcePendingResults = + client.xpending(key, groupName, IdBound.of(streamid_6), IdBound.of(streamid_6), 1L).get(); + assertEquals(streamid_6, forcePendingResults[0][0]); + assertEquals(consumer2, forcePendingResults[0][1]); + assertEquals(99L, forcePendingResults[0][3]); + + // acknowledge streams 2, 3, 4, and 6 and remove them from the xpending results + assertEquals( + 4L, + client + .xack(key, groupName, new String[] {streamid_2, streamid_3, streamid_4, streamid_6}) + .get()); + + pending_results_extended = + client + .xpending(key, groupName, IdBound.ofExclusive(streamid_3), InfRangeBound.MAX, 10L) + .get(); + assertEquals(1, pending_results_extended.length); + assertEquals(streamid_5, pending_results_extended[0][0]); + assertEquals(consumer1, pending_results_extended[0][1]); + + pending_results_extended = + client + .xpending(key, groupName, InfRangeBound.MIN, IdBound.ofExclusive(streamid_5), 10L) + .get(); + assertEquals(1, pending_results_extended.length); + assertEquals(streamid_1, pending_results_extended[0][0]); + assertEquals(consumer1, pending_results_extended[0][1]); + + pending_results_extended = + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + InfRangeBound.MAX, + 10L, + StreamPendingOptions.builder().minIdleTime(1L).consumer(consumer1).build()) + .get(); + // note: streams ID 1 and 5 are still pending, all others were acknowledged + assertEquals(2, pending_results_extended.length); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xpending_xclaim_binary(BaseClient client) { + + GlideString key = gs(UUID.randomUUID().toString()); + GlideString groupName = gs("groupbin" + UUID.randomUUID()); + GlideString zeroStreamId = gs("0"); + GlideString consumer1 = gs("consumer-1-" + UUID.randomUUID()); + GlideString consumer2 = gs("consumer-2-" + UUID.randomUUID()); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer1).get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer2).get()); + + // Add two stream entries for consumer 1 + GlideString streamid_1 = client.xadd(key, Map.of(gs("field1"), gs("value1"))).get(); + assertNotNull(streamid_1); + GlideString streamid_2 = client.xadd(key, Map.of(gs("field2"), gs("value2"))).get(); + assertNotNull(streamid_2); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = client.xreadgroup(Map.of(key, gs(">")), groupName, consumer1).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_1, new GlideString[][] {{gs("field1"), gs("value1")}}, + streamid_2, new GlideString[][] {{gs("field2"), gs("value2")}})), + result_1); + + // Add three stream entries for consumer 2 + GlideString streamid_3 = client.xadd(key, Map.of(gs("field3"), gs("value3"))).get(); + assertNotNull(streamid_3); + GlideString streamid_4 = client.xadd(key, Map.of(gs("field4"), gs("value4"))).get(); + assertNotNull(streamid_4); + GlideString streamid_5 = client.xadd(key, Map.of(gs("field5"), gs("value5"))).get(); + assertNotNull(streamid_5); + + // read the entire stream for the consumer and mark messages as pending + var result_2 = client.xreadgroup(Map.of(key, gs(">")), groupName, consumer2).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_3, new GlideString[][] {{gs("field3"), gs("value3")}}, + streamid_4, new GlideString[][] {{gs("field4"), gs("value4")}}, + streamid_5, new GlideString[][] {{gs("field5"), gs("value5")}})), + result_2); + + Object[] pending_results = client.xpending(key, groupName).get(); + Object[] expectedResult = { + Long.valueOf(5L), + streamid_1, + streamid_5, + new Object[][] {{consumer1, gs("2")}, {consumer2, gs("3")}} + }; + assertDeepEquals(expectedResult, pending_results); + + // ensure idle_time > 0 + Thread.sleep(2000); + Object[][] pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L).get(); + + System.out.println("xpending result:"); + for (int i = 0; i < pending_results_extended.length; i++) { + System.out.println((GlideString) pending_results_extended[i][0]); + } + + // because of idle time return, we have to remove it from the expected results + // and check it separately + assertArrayEquals( + new Object[] {streamid_1, consumer1, 1L}, + ArrayUtils.remove(pending_results_extended[0], 2)); + assertTrue((Long) pending_results_extended[0][2] > 0L); + + assertArrayEquals( + new Object[] {streamid_2, consumer1, 1L}, + ArrayUtils.remove(pending_results_extended[1], 2)); + assertTrue((Long) pending_results_extended[1][2] > 0L); + + assertArrayEquals( + new Object[] {streamid_3, consumer2, 1L}, + ArrayUtils.remove(pending_results_extended[2], 2)); + assertTrue((Long) pending_results_extended[2][2] >= 0L); + + assertArrayEquals( + new Object[] {streamid_4, consumer2, 1L}, + ArrayUtils.remove(pending_results_extended[3], 2)); + assertTrue((Long) pending_results_extended[3][2] >= 0L); + + assertArrayEquals( + new Object[] {streamid_5, consumer2, 1L}, + ArrayUtils.remove(pending_results_extended[4], 2)); + assertTrue((Long) pending_results_extended[4][2] >= 0L); + + // use claim to claim stream 3 and 5 for consumer 1 + var claimResults = + client + .xclaim(key, groupName, consumer1, 0L, new GlideString[] {streamid_3, streamid_5}) + .get(); + assertNotNull(claimResults); + assertEquals(claimResults.size(), 2); + for (var e : claimResults.entrySet()) { + System.out.println("Key: " + e.getKey().getString()); + } + + assertNotNull(claimResults.get(streamid_5)); + assertNotNull(claimResults.get(streamid_3)); + assertDeepEquals( + Map.of( + streamid_3, + new GlideString[][] {{gs("field3"), gs("value3")}}, + streamid_5, + new GlideString[][] {{gs("field5"), gs("value5")}}), + claimResults); + + var claimResultsJustId = + client + .xclaimJustId(key, groupName, consumer1, 0L, new GlideString[] {streamid_3, streamid_5}) + .get(); + assertArrayEquals(new GlideString[] {streamid_3, streamid_5}, claimResultsJustId); + + // add one more stream + GlideString streamid_6 = client.xadd(key, Map.of(gs("field6"), gs("value6"))).get(); + assertNotNull(streamid_6); + + // using force, we can xclaim the message without reading it + var claimForceResults = + client + .xclaim( + key, + groupName, + consumer2, + 0L, + new GlideString[] {streamid_6}, + StreamClaimOptions.builder().force().retryCount(99L).build()) + .get(); + assertDeepEquals( + Map.of(streamid_6, new GlideString[][] {{gs("field6"), gs("value6")}}), claimForceResults); + + Object[][] forcePendingResults = + client.xpending(key, groupName, IdBound.of(streamid_6), IdBound.of(streamid_6), 1L).get(); + assertEquals(streamid_6, forcePendingResults[0][0]); + assertEquals(consumer2, forcePendingResults[0][1]); + assertEquals(99L, forcePendingResults[0][3]); + + // acknowledge streams 2, 3, 4, and 6 and remove them from the xpending results + assertEquals( + 4L, + client + .xack( + key, groupName, new GlideString[] {streamid_2, streamid_3, streamid_4, streamid_6}) + .get()); + + pending_results_extended = + client + .xpending(key, groupName, IdBound.ofExclusive(streamid_3), InfRangeBound.MAX, 10L) + .get(); + assertEquals(1, pending_results_extended.length); + assertEquals(streamid_5, pending_results_extended[0][0]); + assertEquals(consumer1, pending_results_extended[0][1]); + + pending_results_extended = + client + .xpending(key, groupName, InfRangeBound.MIN, IdBound.ofExclusive(streamid_5), 10L) + .get(); + assertEquals(1, pending_results_extended.length); + assertEquals(streamid_1, pending_results_extended[0][0]); + assertEquals(consumer1, pending_results_extended[0][1]); + + pending_results_extended = + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + InfRangeBound.MAX, + 10L, + StreamPendingOptionsBinary.builder().minIdleTime(1L).consumer(consumer1).build()) + .get(); + // note: streams ID 1 and 5 are still pending, all others were acknowledged + assertEquals(2, pending_results_extended.length); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xpending_return_failures(BaseClient client) { + + String key = UUID.randomUUID().toString(); + String stringkey = UUID.randomUUID().toString(); + String groupName = "group" + UUID.randomUUID(); + String zeroStreamId = "0"; + String consumer1 = "consumer-1-" + UUID.randomUUID(); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer1).get()); + + // Add two stream entries for consumer 1 + String streamid_1 = client.xadd(key, Map.of("field1", "value1")).get(); + assertNotNull(streamid_1); + String streamid_2 = client.xadd(key, Map.of("field2", "value2")).get(); + assertNotNull(streamid_2); + + // no pending messages yet... + var pending_results_summary = client.xpending(key, groupName).get(); + assertArrayEquals(new Object[] {0L, null, null, null}, pending_results_summary); + + var pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, pending_results_extended.length); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = client.xreadgroup(Map.of(key, ">"), groupName, consumer1).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_1, new String[][] {{"field1", "value1"}}, + streamid_2, new String[][] {{"field2", "value2"}})), + result_1); + + // sanity check - expect some results: + pending_results_summary = client.xpending(key, groupName).get(); + assertTrue((Long) pending_results_summary[0] > 0L); + + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 1L).get(); + assertTrue(pending_results_extended.length > 0); + + // returns empty if + before - + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, pending_results_extended.length); + + // min idletime of 100 seconds shouldn't produce any results + pending_results_extended = + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + InfRangeBound.MAX, + 10L, + StreamPendingOptions.builder().minIdleTime(100000L).build()) + .get(); + assertEquals(0, pending_results_extended.length); + + // invalid consumer - no results + pending_results_extended = + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + InfRangeBound.MAX, + 10L, + StreamPendingOptions.builder().consumer("invalid_consumer").build()) + .get(); + assertEquals(0, pending_results_extended.length); + + // xpending when range bound is not valid ID throws a RequestError + Exception executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending( + key, + groupName, + IdBound.ofExclusive("not_a_stream_id"), + InfRangeBound.MAX, + 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + IdBound.ofExclusive("not_a_stream_id"), + 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // invalid count should return no results + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, -10L).get(); + assertEquals(0, pending_results_extended.length); + + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 0L).get(); + assertEquals(0, pending_results_extended.length); + + // invalid group throws a RequestError (NOGROUP) + executionException = + assertThrows(ExecutionException.class, () -> client.xpending(key, "not_a_group").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // non-existent key throws a RequestError (NOGROUP) + executionException = + assertThrows(ExecutionException.class, () -> client.xpending(stringkey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending(stringkey, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(stringkey, "bar").get()); + executionException = + assertThrows(ExecutionException.class, () -> client.xpending(stringkey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending(stringkey, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xpending_binary_return_failures(BaseClient client) { + + GlideString key = gs(UUID.randomUUID().toString()); + GlideString stringkey = gs(UUID.randomUUID().toString()); + GlideString groupName = gs("group" + UUID.randomUUID()); + GlideString zeroStreamId = gs("0"); + GlideString consumer1 = gs("consumer-1-" + UUID.randomUUID()); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer1).get()); + + // Add two stream entries for consumer 1 + GlideString streamid_1 = client.xadd(key, Map.of(gs("field1"), gs("value1"))).get(); + assertNotNull(streamid_1); + GlideString streamid_2 = client.xadd(key, Map.of(gs("field2"), gs("value2"))).get(); + assertNotNull(streamid_2); + + // no pending messages yet... + var pending_results_summary = client.xpending(key, groupName).get(); + assertArrayEquals(new Object[] {0L, null, null, null}, pending_results_summary); + + var pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, pending_results_extended.length); + + // read the entire stream for the consumer and mark messages as pending + var result_1 = client.xreadgroup(Map.of(key, gs(">")), groupName, consumer1).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_1, new GlideString[][] {{gs("field1"), gs("value1")}}, + streamid_2, new GlideString[][] {{gs("field2"), gs("value2")}})), + result_1); + + // sanity check - expect some results: + pending_results_summary = client.xpending(key, groupName).get(); + assertTrue((Long) pending_results_summary[0] > 0L); + + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 1L).get(); + assertTrue(pending_results_extended.length > 0); + + // returns empty if + before - + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MAX, InfRangeBound.MIN, 10L).get(); + assertEquals(0, pending_results_extended.length); + + // min idletime of 100 seconds shouldn't produce any results + pending_results_extended = + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + InfRangeBound.MAX, + 10L, + StreamPendingOptionsBinary.builder().minIdleTime(100000L).build()) + .get(); + assertEquals(0, pending_results_extended.length); + + // invalid consumer - no results + pending_results_extended = + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + InfRangeBound.MAX, + 10L, + StreamPendingOptionsBinary.builder().consumer(gs("invalid_consumer")).build()) + .get(); + assertEquals(0, pending_results_extended.length); + + // xpending when range bound is not valid ID throws a RequestError + Exception executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending( + key, + groupName, + IdBound.ofExclusive("not_a_stream_id"), + InfRangeBound.MAX, + 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending( + key, + groupName, + InfRangeBound.MIN, + IdBound.ofExclusive(gs("not_a_stream_id")), + 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // invalid count should return no results + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, -10L).get(); + assertEquals(0, pending_results_extended.length); + + pending_results_extended = + client.xpending(key, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 0L).get(); + assertEquals(0, pending_results_extended.length); + + // invalid group throws a RequestError (NOGROUP) + executionException = + assertThrows(ExecutionException.class, () -> client.xpending(key, gs("not_a_group")).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // non-existent key throws a RequestError (NOGROUP) + executionException = + assertThrows(ExecutionException.class, () -> client.xpending(stringkey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending(stringkey, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(stringkey, gs("bar")).get()); + executionException = + assertThrows(ExecutionException.class, () -> client.xpending(stringkey, groupName).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xpending(stringkey, groupName, InfRangeBound.MIN, InfRangeBound.MAX, 10L) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xclaim_return_failures(BaseClient client) { + + String key = UUID.randomUUID().toString(); + String stringkey = UUID.randomUUID().toString(); + String groupName = "group" + UUID.randomUUID(); + String zeroStreamId = "0"; + String consumer1 = "consumer-1-" + UUID.randomUUID(); + String consumer2 = "consumer-2-" + UUID.randomUUID(); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer1).get()); + + // Add stream entry and mark as pending: + String streamid_1 = client.xadd(key, Map.of("field1", "value1")).get(); + assertNotNull(streamid_1); + assertNotNull(client.xreadgroup(Map.of(key, ">"), groupName, consumer1).get()); + + // claim with invalid stream entry IDs + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client.xclaimJustId(key, groupName, consumer1, 1L, new String[] {"invalid"}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // claim with empty stream entry IDs returns no results + var emptyClaim = client.xclaimJustId(key, groupName, consumer1, 1L, new String[0]).get(); + assertEquals(0L, emptyClaim.length); + + // non-existent key throws a RequestError (NOGROUP) + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim(stringkey, groupName, consumer1, 1L, new String[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + final var claimOptions = StreamClaimOptions.builder().idle(1L).build(); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim( + stringkey, + groupName, + consumer1, + 1L, + new String[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId(stringkey, groupName, consumer1, 1L, new String[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId( + stringkey, + groupName, + consumer1, + 1L, + new String[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(stringkey, "bar").get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim(stringkey, groupName, consumer1, 1L, new String[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim( + stringkey, + groupName, + consumer1, + 1L, + new String[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId(stringkey, groupName, consumer1, 1L, new String[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId( + stringkey, + groupName, + consumer1, + 1L, + new String[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xclaim_binary_return_failures(BaseClient client) { + + GlideString key = gs(UUID.randomUUID().toString()); + GlideString stringkey = gs(UUID.randomUUID().toString()); + GlideString groupName = gs("group" + UUID.randomUUID()); + GlideString zeroStreamId = gs("0"); + GlideString consumer1 = gs("consumer-1-" + UUID.randomUUID()); + GlideString consumer2 = gs("consumer-2-" + UUID.randomUUID()); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + assertTrue(client.xgroupCreateConsumer(key, groupName, consumer1).get()); + + // Add stream entry and mark as pending: + GlideString streamid_1 = client.xadd(key, Map.of(gs("field1"), gs("value1"))).get(); + assertNotNull(streamid_1); + assertNotNull(client.xreadgroup(Map.of(key, gs(">")), groupName, consumer1).get()); + + // claim with invalid stream entry IDs + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId(key, groupName, consumer1, 1L, new GlideString[] {gs("invalid")}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // claim with empty stream entry IDs returns no results + var emptyClaim = client.xclaimJustId(key, groupName, consumer1, 1L, new GlideString[0]).get(); + assertEquals(0L, emptyClaim.length); + + // non-existent key throws a RequestError (NOGROUP) + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim(stringkey, groupName, consumer1, 1L, new GlideString[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + final var claimOptions = StreamClaimOptions.builder().idle(1L).build(); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim( + stringkey, + groupName, + consumer1, + 1L, + new GlideString[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId( + stringkey, groupName, consumer1, 1L, new GlideString[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId( + stringkey, + groupName, + consumer1, + 1L, + new GlideString[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("NOGROUP")); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(stringkey, gs("bar")).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim(stringkey, groupName, consumer1, 1L, new GlideString[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaim( + stringkey, + groupName, + consumer1, + 1L, + new GlideString[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId( + stringkey, groupName, consumer1, 1L, new GlideString[] {streamid_1}) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .xclaimJustId( + stringkey, + groupName, + consumer1, + 1L, + new GlideString[] {streamid_1}, + claimOptions) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void xautoclaim(BaseClient client) { + String minVersion = "6.2.0"; + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo(minVersion), + "This feature added in valkey " + minVersion); + String key = UUID.randomUUID().toString(); + String groupName = UUID.randomUUID().toString(); + String zeroStreamId = "0-0"; + String consumer = UUID.randomUUID().toString(); + String consumer2 = UUID.randomUUID().toString(); + + // Add 4 stream entries for consumer + String streamid_0 = client.xadd(key, Map.of("f1", "v1" /*, "f2", "v2"*/)).get(); + assertNotNull(streamid_0); + String streamid_1 = client.xadd(key, Map.of("field1", "value1")).get(); + assertNotNull(streamid_1); + String streamid_2 = client.xadd(key, Map.of("field2", "value2")).get(); + assertNotNull(streamid_2); + String streamid_3 = client.xadd(key, Map.of("field3", "value3")).get(); + assertNotNull(streamid_3); + + // create group and consumer for the group + assertEquals( + OK, + client + .xgroupCreate( + key, groupName, zeroStreamId, StreamGroupOptions.builder().makeStream().build()) + .get()); + + // read the entire stream for the consumer and mark messages as pending + var xreadgroup_result = client.xreadgroup(Map.of(key, ">"), groupName, consumer).get(); + assertDeepEquals( + Map.of( + key, + Map.of( + streamid_0, new String[][] {{"f1", "v1"} /*, {"f2", "v2"}*/}, + streamid_1, new String[][] {{"field1", "value1"}}, + streamid_2, new String[][] {{"field2", "value2"}}, + streamid_3, new String[][] {{"field3", "value3"}})), + xreadgroup_result); + + Object[] xautoclaimResult1 = + client.xautoclaim(key, groupName, consumer, 0L, zeroStreamId, 1L).get(); + assertEquals(streamid_1, xautoclaimResult1[0]); + assertDeepEquals( + Map.of(streamid_0, new String[][] {{"f1", "v1"} /*, {"f2", "v2"}*/}), xautoclaimResult1[1]); + + // if using Valkey 7.0.0 or above, responses also include a list of entry IDs that were removed + // from the Pending + // Entries List because they no longer exist in the stream + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertDeepEquals(new Object[] {}, xautoclaimResult1[2]); + } + + // delete entry 1-2 + assertEquals(1, client.xdel(key, new String[] {streamid_2}).get()); + + // autoclaim the rest of the entries + Object[] xautoclaimResult2 = + client.xautoclaim(gs(key), gs(groupName), gs(consumer), 0L, gs(streamid_1)).get(); + assertEquals( + gs(zeroStreamId), + xautoclaimResult2[0]); // "0-0" is returned to indicate the entire stream was scanned. + assertDeepEquals( + Map.of( + gs(streamid_1), new GlideString[][] {{gs("field1"), gs("value1")}}, + gs(streamid_3), new GlideString[][] {{gs("field3"), gs("value3")}}), + xautoclaimResult2[1]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertDeepEquals(new GlideString[] {gs(streamid_2)}, xautoclaimResult2[2]); + } + + // autoclaim with JUSTID: result at index 1 does not contain fields/values of the claimed + // entries, only IDs + Object[] justIdResult = + client.xautoclaimJustId(key, groupName, consumer, 0L, zeroStreamId).get(); + assertEquals(zeroStreamId, justIdResult[0]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertDeepEquals(new String[] {streamid_0, streamid_1, streamid_3}, justIdResult[1]); + assertDeepEquals(new Object[] {}, justIdResult[2]); + } else { + // in valkey < 7.0.0, specifically for XAUTOCLAIM with JUSTID, entry IDs that were in the + // Pending Entries List + // but are no longer in the stream still show up in the response + assertDeepEquals( + new String[] {streamid_0, streamid_1, streamid_2, streamid_3}, justIdResult[1]); + } + + Object[] justIdResultCount = + client + .xautoclaimJustId(gs(key), gs(groupName), gs(consumer2), 0L, gs(zeroStreamId), 1L) + .get(); + assertEquals(gs(streamid_1), justIdResultCount[0]); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertDeepEquals(new GlideString[] {gs(streamid_0)}, justIdResultCount[1]); + assertDeepEquals(new Object[] {}, justIdResultCount[2]); + } else { + assertDeepEquals(new GlideString[] {gs(streamid_0)}, justIdResultCount[1]); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmember(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + String randMember = client.zrandmember(key1).get(); + assertTrue(membersScores.containsKey(randMember)); + assertNull(client.zrandmember("nonExistentKey").get()); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrandmember(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmemberWithCount(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + // Unique values are expected as count is positive + List randMembers = Arrays.asList(client.zrandmemberWithCount(key1, 4).get()); + assertEquals(2, randMembers.size()); + assertEquals(2, new HashSet<>(randMembers).size()); + randMembers.forEach(member -> assertTrue(membersScores.containsKey(member))); + + // Duplicate values are expected as count is negative + randMembers = Arrays.asList(client.zrandmemberWithCount(key1, -4).get()); + assertEquals(4, randMembers.size()); + randMembers.forEach(member -> assertTrue(membersScores.containsKey(member))); + + assertEquals(0, client.zrandmemberWithCount(key1, 0).get().length); + assertEquals(0, client.zrandmemberWithCount("nonExistentKey", 4).get().length); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrandmemberWithCount(key2, 5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrandmemberWithCountWithScores(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0); + assertEquals(2, client.zadd(key1, membersScores).get()); + + // Unique values are expected as count is positive + Object[][] randMembersWithScores = client.zrandmemberWithCountWithScores(key1, 4).get(); + assertEquals(2, randMembersWithScores.length); + for (Object[] membersWithScore : randMembersWithScores) { + String member = (String) membersWithScore[0]; + Double score = (Double) membersWithScore[1]; + + assertEquals(score, membersScores.get(member)); + } + + // Duplicate values are expected as count is negative + randMembersWithScores = client.zrandmemberWithCountWithScores(key1, -4).get(); + assertEquals(4, randMembersWithScores.length); + for (Object[] randMembersWithScore : randMembersWithScores) { + String member = (String) randMembersWithScore[0]; + Double score = (Double) randMembersWithScore[1]; + + assertEquals(score, membersScores.get(member)); + } + + assertEquals(0, client.zrandmemberWithCountWithScores(key1, 0).get().length); + assertEquals(0, client.zrandmemberWithCountWithScores("nonExistentKey", 4).get().length); + + // Key exists, but it is not a set + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.zrandmemberWithCountWithScores(key2, 5).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zincrby(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + // key does not exist + assertEquals(2.5, client.zincrby(key1, 2.5, "value1").get()); + assertEquals(2.5, client.zscore(key1, "value1").get()); + + // key exists, but value doesn't + assertEquals(-3.3, client.zincrby(key1, -3.3, "value2").get()); + assertEquals(-3.3, client.zscore(key1, "value2").get()); + + // updating existing value in existing key + assertEquals(3.5, client.zincrby(key1, 1., "value1").get()); + assertEquals(3.5, client.zscore(key1, "value1").get()); + + // Key exists, but it is not a sorted set + assertEquals(2L, client.sadd(key2, new String[] {"one", "two"}).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zincrby(key2, .5, "_").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void type(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String listKey = UUID.randomUUID().toString(); + String hashKey = UUID.randomUUID().toString(); + String setKey = UUID.randomUUID().toString(); + String zsetKey = UUID.randomUUID().toString(); + String streamKey = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(stringKey, "value").get()); + assertEquals(1, client.lpush(listKey, new String[] {"value"}).get()); + assertEquals(1, client.hset(hashKey, Map.of("1", "2")).get()); + assertEquals(1, client.sadd(setKey, new String[] {"value"}).get()); + assertEquals(1, client.zadd(zsetKey, Map.of("1", 2d)).get()); + assertNotNull(client.xadd(streamKey, Map.of("field", "value"))); + + assertTrue("none".equalsIgnoreCase(client.type(nonExistingKey).get())); + assertTrue("string".equalsIgnoreCase(client.type(stringKey).get())); + assertTrue("list".equalsIgnoreCase(client.type(listKey).get())); + assertTrue("hash".equalsIgnoreCase(client.type(hashKey).get())); + assertTrue("set".equalsIgnoreCase(client.type(setKey).get())); + assertTrue("zset".equalsIgnoreCase(client.type(zsetKey).get())); + assertTrue("stream".equalsIgnoreCase(client.type(streamKey).get())); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void type_binary(BaseClient client) { + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString listKey = gs(UUID.randomUUID().toString()); + String hashKey = UUID.randomUUID().toString(); + String setKey = UUID.randomUUID().toString(); + String zsetKey = UUID.randomUUID().toString(); + String streamKey = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(stringKey, gs("value")).get()); + assertEquals(1, client.lpush(listKey, new GlideString[] {gs("value")}).get()); + assertEquals(1, client.hset(hashKey, Map.of("1", "2")).get()); + assertEquals(1, client.sadd(setKey, new String[] {"value"}).get()); + assertEquals(1, client.zadd(zsetKey, Map.of("1", 2d)).get()); + assertNotNull(client.xadd(streamKey, Map.of("field", "value"))); + + assertTrue("none".equalsIgnoreCase(client.type(nonExistingKey).get())); + assertTrue("string".equalsIgnoreCase(client.type(stringKey).get())); + assertTrue("list".equalsIgnoreCase(client.type(listKey).get())); + assertTrue("hash".equalsIgnoreCase(client.type(hashKey).get())); + assertTrue("set".equalsIgnoreCase(client.type(setKey).get())); + assertTrue("zset".equalsIgnoreCase(client.type(zsetKey).get())); + assertTrue("stream".equalsIgnoreCase(client.type(streamKey).get())); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void linsert(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + assertEquals(4, client.lpush(key1, new String[] {"4", "3", "2", "1"}).get()); + assertEquals(5, client.linsert(key1, BEFORE, "2", "1.5").get()); + assertEquals(6, client.linsert(key1, AFTER, "3", "3.5").get()); + assertArrayEquals( + new String[] {"1", "1.5", "2", "3", "3.5", "4"}, client.lrange(key1, 0, -1).get()); + + assertEquals(0, client.linsert(key2, BEFORE, "pivot", "elem").get()); + assertEquals(-1, client.linsert(key1, AFTER, "5", "6").get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set(key2, "linsert").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.linsert(key2, AFTER, "p", "e").get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_binary_with_different_types_of_keys(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + RangeByIndex query = new RangeByIndex(0, 1); + + assertArrayEquals(new GlideString[] {}, client.zrange(gs("non_existing_key"), query).get()); + + assertTrue( + client + .zrangeWithScores(gs("non_existing_key"), query) + .get() + .isEmpty()); // start is greater than stop + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, gs("value")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrange(key, query).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows(ExecutionException.class, () -> client.zrangeWithScores(key, query).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void linsert_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + + assertEquals( + 4, client.lpush(key1, new GlideString[] {gs("4"), gs("3"), gs("2"), gs("1")}).get()); + assertEquals(5, client.linsert(key1, BEFORE, gs("2"), gs("1.5")).get()); + assertEquals(6, client.linsert(key1, AFTER, gs("3"), gs("3.5")).get()); + assertArrayEquals( + new String[] {"1", "1.5", "2", "3", "3.5", "4"}, + client.lrange(key1.toString(), 0, -1).get()); + + assertEquals(0, client.linsert(key2, BEFORE, gs("pivot"), gs("elem")).get()); + assertEquals(-1, client.linsert(key1, AFTER, gs("5"), gs("6")).get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set(key2, gs("linsert")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.linsert(key2, AFTER, gs("p"), gs("e")).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void brpop(BaseClient client) { + String listKey1 = "{listKey}-1-" + UUID.randomUUID(); + String listKey2 = "{listKey}-2-" + UUID.randomUUID(); + String value1 = "value1-" + UUID.randomUUID(); + String value2 = "value2-" + UUID.randomUUID(); + assertEquals(2, client.lpush(listKey1, new String[] {value1, value2}).get()); + + var response = client.brpop(new String[] {listKey1, listKey2}, 0.5).get(); + assertArrayEquals(new String[] {listKey1, value1}, response); + + // nothing popped out + assertNull( + client + .brpop(new String[] {listKey2}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.brpop(new String[] {"foo"}, .0001).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void brpop_binary(BaseClient client) { + GlideString listKey1 = gs("{listKey}-1-" + UUID.randomUUID()); + GlideString listKey2 = gs("{listKey}-2-" + UUID.randomUUID()); + GlideString value1 = gs("value1-" + UUID.randomUUID()); + GlideString value2 = gs("value2-" + UUID.randomUUID()); + assertEquals(2, client.lpush(listKey1, new GlideString[] {value1, value2}).get()); + + var response = client.brpop(new GlideString[] {listKey1, listKey2}, 0.5).get(); + assertArrayEquals(new GlideString[] {listKey1, value1}, response); + + // nothing popped out + assertNull( + client + .brpop(new GlideString[] {listKey2}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.brpop(new GlideString[] {gs("foo")}, .0001).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void rpushx(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String key3 = UUID.randomUUID().toString(); + + assertEquals(1, client.rpush(key1, new String[] {"0"}).get()); + assertEquals(4, client.rpushx(key1, new String[] {"1", "2", "3"}).get()); + assertArrayEquals(new String[] {"0", "1", "2", "3"}, client.lrange(key1, 0, -1).get()); + + assertEquals(0, client.rpushx(key2, new String[] {"1"}).get()); + assertArrayEquals(new String[0], client.lrange(key2, 0, -1).get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set(key3, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.rpushx(key3, new String[] {"_"}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + // empty element list + executionException = + assertThrows(ExecutionException.class, () -> client.rpushx(key2, new String[0]).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blpop(BaseClient client) { + String listKey1 = "{listKey}-1-" + UUID.randomUUID(); + String listKey2 = "{listKey}-2-" + UUID.randomUUID(); + String value1 = "value1-" + UUID.randomUUID(); + String value2 = "value2-" + UUID.randomUUID(); + assertEquals(2, client.lpush(listKey1, new String[] {value1, value2}).get()); + + var response = client.blpop(new String[] {listKey1, listKey2}, 0.5).get(); + assertArrayEquals(new String[] {listKey1, value2}, response); + + // nothing popped out + assertNull( + client + .blpop(new String[] {listKey2}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.blpop(new String[] {"foo"}, .0001).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blpop_binary(BaseClient client) { + GlideString listKey1 = gs("{listKey}-1-" + UUID.randomUUID()); + GlideString listKey2 = gs("{listKey}-2-" + UUID.randomUUID()); + GlideString value1 = gs("value1-" + UUID.randomUUID()); + GlideString value2 = gs("value2-" + UUID.randomUUID()); + assertEquals(2, client.lpush(listKey1, new GlideString[] {value1, value2}).get()); + + var response = client.blpop(new GlideString[] {listKey1, listKey2}, 0.5).get(); + assertArrayEquals(new GlideString[] {listKey1, value2}, response); + + // nothing popped out + assertNull( + client + .blpop(new GlideString[] {listKey2}, SERVER_VERSION.isLowerThan("7.0.0") ? 1. : 0.001) + .get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set(gs("foo"), gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.blpop(new GlideString[] {gs("foo")}, .0001).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lpushx(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String key3 = UUID.randomUUID().toString(); + + assertEquals(1, client.lpush(key1, new String[] {"0"}).get()); + assertEquals(4, client.lpushx(key1, new String[] {"1", "2", "3"}).get()); + assertArrayEquals(new String[] {"3", "2", "1", "0"}, client.lrange(key1, 0, -1).get()); + + assertEquals(0, client.lpushx(key2, new String[] {"1"}).get()); + assertArrayEquals(new String[0], client.lrange(key2, 0, -1).get()); + + // Key exists, but it is not a list + assertEquals(OK, client.set(key3, "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lpushx(key3, new String[] {"_"}).get()); + // empty element list + executionException = + assertThrows(ExecutionException.class, () -> client.lpushx(key2, new String[0]).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_by_index(BaseClient client) { + String key = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + + RangeByIndex query = new RangeByIndex(0, 1); + assertArrayEquals(new String[] {"one", "two"}, client.zrange(key, query).get()); + + query = new RangeByIndex(0, -1); + assertEquals( + Map.of("one", 1.0, "two", 2.0, "three", 3.0), client.zrangeWithScores(key, query).get()); + + query = new RangeByIndex(0, 1); + assertArrayEquals(new String[] {"three", "two"}, client.zrange(key, query, true).get()); + + query = new RangeByIndex(3, 1); + assertArrayEquals(new String[] {}, client.zrange(key, query, true).get()); + assertTrue(client.zrangeWithScores(key, query).get().isEmpty()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_binary_by_index(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + + RangeByIndex query = new RangeByIndex(0, 1); + assertArrayEquals(new GlideString[] {gs("one"), gs("two")}, client.zrange(key, query).get()); + + query = new RangeByIndex(0, -1); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0), + client.zrangeWithScores(key, query).get()); + + query = new RangeByIndex(0, 1); + assertArrayEquals( + new GlideString[] {gs("three"), gs("two")}, client.zrange(key, query, true).get()); + + query = new RangeByIndex(3, 1); + assertArrayEquals(new GlideString[] {}, client.zrange(key, query, true).get()); + assertTrue(client.zrangeWithScores(key, query).get().isEmpty()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_by_score(BaseClient client) { + String key = UUID.randomUUID().toString(); + Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + + RangeByScore query = new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertArrayEquals(new String[] {"one", "two"}, client.zrange(key, query).get()); + + query = new RangeByScore(NEGATIVE_INFINITY, POSITIVE_INFINITY); + assertEquals( + Map.of("one", 1.0, "two", 2.0, "three", 3.0), client.zrangeWithScores(key, query).get()); + + query = new RangeByScore(new ScoreBoundary(3, false), NEGATIVE_INFINITY); + assertArrayEquals(new String[] {"two", "one"}, client.zrange(key, query, true).get()); + + query = new RangeByScore(NEGATIVE_INFINITY, POSITIVE_INFINITY, new Limit(1, 2)); + assertArrayEquals(new String[] {"two", "three"}, client.zrange(key, query).get()); + + query = new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertArrayEquals( + new String[] {}, + client + .zrange(key, query, true) + .get()); // stop is greater than start with reverse set to True + + query = new RangeByScore(POSITIVE_INFINITY, new ScoreBoundary(3, false)); + assertArrayEquals( + new String[] {}, client.zrange(key, query, true).get()); // start is greater than stop + + query = new RangeByScore(POSITIVE_INFINITY, new ScoreBoundary(3, false)); + assertTrue(client.zrangeWithScores(key, query).get().isEmpty()); // start is greater than stop + + query = new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertTrue( + client + .zrangeWithScores(key, query, true) + .get() + .isEmpty()); // stop is greater than start with reverse set to True + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_binary_by_score(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + + RangeByScore query = + new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertArrayEquals(new GlideString[] {gs("one"), gs("two")}, client.zrange(key, query).get()); + + query = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, InfScoreBound.POSITIVE_INFINITY); + assertEquals( + Map.of(gs("one"), 1.0, gs("two"), 2.0, gs("three"), 3.0), + client.zrangeWithScores(key, query).get()); + + query = new RangeByScore(new ScoreBoundary(3, false), InfScoreBound.NEGATIVE_INFINITY); + assertArrayEquals( + new GlideString[] {gs("two"), gs("one")}, client.zrange(key, query, true).get()); + + query = + new RangeByScore( + InfScoreBound.NEGATIVE_INFINITY, InfScoreBound.POSITIVE_INFINITY, new Limit(1, 2)); + assertArrayEquals(new GlideString[] {gs("two"), gs("three")}, client.zrange(key, query).get()); + + query = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertArrayEquals( + new GlideString[] {}, + client + .zrange(key, query, true) + .get()); // stop is greater than start with reverse set to True + + query = new RangeByScore(InfScoreBound.POSITIVE_INFINITY, new ScoreBoundary(3, false)); + assertArrayEquals( + new GlideString[] {}, client.zrange(key, query, true).get()); // start is greater than stop + + query = new RangeByScore(InfScoreBound.POSITIVE_INFINITY, new ScoreBoundary(3, false)); + assertTrue(client.zrangeWithScores(key, query).get().isEmpty()); // start is greater than stop + + query = new RangeByScore(InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false)); + assertTrue( + client + .zrangeWithScores(key, query, true) + .get() + .isEmpty()); // stop is greater than start with reverse set to True + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_by_lex(BaseClient client) { + String key = UUID.randomUUID().toString(); + Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + + RangeByLex query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + assertArrayEquals(new String[] {"a", "b"}, client.zrange(key, query).get()); + + query = + new RangeByLex( + InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY, new Limit(1, 2)); + assertArrayEquals(new String[] {"b", "c"}, client.zrange(key, query).get()); + + query = new RangeByLex(new LexBoundary("c", false), InfLexBound.NEGATIVE_INFINITY); + assertArrayEquals(new String[] {"b", "a"}, client.zrange(key, query, true).get()); + + query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + assertArrayEquals( + new String[] {}, + client + .zrange(key, query, true) + .get()); // stop is greater than start with reverse set to True + + query = new RangeByLex(InfLexBound.POSITIVE_INFINITY, new LexBoundary("c", false)); + assertArrayEquals( + new String[] {}, client.zrange(key, query).get()); // start is greater than stop + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_binary_by_lex(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + Map membersScores = Map.of(gs("a"), 1.0, gs("b"), 2.0, gs("c"), 3.0); + assertEquals(3, client.zadd(key, membersScores).get()); + + RangeByLex query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + assertArrayEquals(new GlideString[] {gs("a"), gs("b")}, client.zrange(key, query).get()); + + query = + new RangeByLex( + InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY, new Limit(1, 2)); + assertArrayEquals(new GlideString[] {gs("b"), gs("c")}, client.zrange(key, query).get()); + + query = new RangeByLex(new LexBoundary("c", false), InfLexBound.NEGATIVE_INFINITY); + assertArrayEquals(new GlideString[] {gs("b"), gs("a")}, client.zrange(key, query, true).get()); + + query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); + assertArrayEquals( + new GlideString[] {}, + client + .zrange(key, query, true) + .get()); // stop is greater than start with reverse set to True + + query = new RangeByLex(InfLexBound.POSITIVE_INFINITY, new LexBoundary("c", false)); + assertArrayEquals( + new GlideString[] {}, client.zrange(key, query).get()); // start is greater than stop + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zrange_with_different_types_of_keys(BaseClient client) { + String key = UUID.randomUUID().toString(); + RangeByIndex query = new RangeByIndex(0, 1); + + assertArrayEquals(new String[] {}, client.zrange("non_existing_key", query).get()); + + assertTrue( + client + .zrangeWithScores("non_existing_key", query) + .get() + .isEmpty()); // start is greater than stop + + // Key exists, but it is not a set + assertEquals(OK, client.set(key, "value").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zrange(key, query).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows(ExecutionException.class, () -> client.zrangeWithScores(key, query).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void pfadd(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(1, client.pfadd(key, new String[0]).get()); + assertEquals(1, client.pfadd(key, new String[] {"one", "two"}).get()); + assertEquals(0, client.pfadd(key, new String[] {"two"}).get()); + assertEquals(0, client.pfadd(key, new String[0]).get()); + + // Key exists, but it is not a HyperLogLog + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.pfadd("foo", new String[0]).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void pfadd_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + assertEquals(1, client.pfadd(key, new GlideString[0]).get()); + assertEquals(1, client.pfadd(key, new GlideString[] {gs("one"), gs("two")}).get()); + assertEquals(0, client.pfadd(key, new GlideString[] {gs("two")}).get()); + assertEquals(0, client.pfadd(key, new GlideString[0]).get()); + + // Key exists, but it is not a HyperLogLog + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.pfadd(gs("foo"), new GlideString[0]).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void pfcount(BaseClient client) { + String key1 = "{test}-hll1-" + UUID.randomUUID(); + String key2 = "{test}-hll2-" + UUID.randomUUID(); + String key3 = "{test}-hll3-" + UUID.randomUUID(); + assertEquals(1, client.pfadd(key1, new String[] {"a", "b", "c"}).get()); + assertEquals(1, client.pfadd(key2, new String[] {"b", "c", "d"}).get()); + assertEquals(3, client.pfcount(new String[] {key1}).get()); + assertEquals(3, client.pfcount(new String[] {key2}).get()); + assertEquals(4, client.pfcount(new String[] {key1, key2}).get()); + assertEquals(4, client.pfcount(new String[] {key1, key2, key3}).get()); + // empty HyperLogLog data set + assertEquals(1, client.pfadd(key3, new String[0]).get()); + assertEquals(0, client.pfcount(new String[] {key3}).get()); + + // Key exists, but it is not a HyperLogLog + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.pfcount(new String[] {"foo"}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void pfcount_binary(BaseClient client) { + GlideString key1 = gs("{test}-hll1-" + UUID.randomUUID()); + GlideString key2 = gs("{test}-hll2-" + UUID.randomUUID()); + GlideString key3 = gs("{test}-hll3-" + UUID.randomUUID()); + assertEquals(1, client.pfadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(1, client.pfadd(key2, new GlideString[] {gs("b"), gs("c"), gs("d")}).get()); + assertEquals(3, client.pfcount(new GlideString[] {key1}).get()); + assertEquals(3, client.pfcount(new GlideString[] {key2}).get()); + assertEquals(4, client.pfcount(new GlideString[] {key1, key2}).get()); + assertEquals(4, client.pfcount(new GlideString[] {key1, key2, key3}).get()); + // empty HyperLogLog data set + assertEquals(1, client.pfadd(key3, new GlideString[0]).get()); + assertEquals(0, client.pfcount(new GlideString[] {key3}).get()); + + // Key exists, but it is not a HyperLogLog + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.pfcount(new GlideString[] {gs("foo")}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void pfmerge(BaseClient client) { + String key1 = "{test}-hll1-" + UUID.randomUUID(); + String key2 = "{test}-hll2-" + UUID.randomUUID(); + String key3 = "{test}-hll3-" + UUID.randomUUID(); + assertEquals(1, client.pfadd(key1, new String[] {"a", "b", "c"}).get()); + assertEquals(1, client.pfadd(key2, new String[] {"b", "c", "d"}).get()); + // new HyperLogLog data set + assertEquals(OK, client.pfmerge(key3, new String[] {key1, key2}).get()); + assertEquals( + client.pfcount(new String[] {key1, key2}).get(), client.pfcount(new String[] {key3}).get()); + // existing HyperLogLog data set + assertEquals(OK, client.pfmerge(key1, new String[] {key2}).get()); + assertEquals( + client.pfcount(new String[] {key1, key2}).get(), client.pfcount(new String[] {key1}).get()); + + // Key exists, but it is not a HyperLogLog + assertEquals(OK, client.set("foo", "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.pfmerge("foo", new String[] {key1}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + executionException = + assertThrows( + ExecutionException.class, () -> client.pfmerge(key1, new String[] {"foo"}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void pfmerge_binary(BaseClient client) { + GlideString key1 = gs("{test}-hll1-" + UUID.randomUUID()); + GlideString key2 = gs("{test}-hll2-" + UUID.randomUUID()); + GlideString key3 = gs("{test}-hll3-" + UUID.randomUUID()); + assertEquals(1, client.pfadd(key1, new GlideString[] {gs("a"), gs("b"), gs("c")}).get()); + assertEquals(1, client.pfadd(key2, new GlideString[] {gs("b"), gs("c"), gs("d")}).get()); + // new HyperLogLog data set + assertEquals(OK, client.pfmerge(key3, new GlideString[] {key1, key2}).get()); + assertEquals( + client.pfcount(new GlideString[] {key1, key2}).get(), + client.pfcount(new GlideString[] {key3}).get()); + // existing HyperLogLog data set + assertEquals(OK, client.pfmerge(key1, new GlideString[] {key2}).get()); + assertEquals( + client.pfcount(new GlideString[] {key1, key2}).get(), + client.pfcount(new GlideString[] {key1}).get()); + + // Key exists, but it is not a HyperLogLog + assertEquals(OK, client.set(gs("foo"), gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.pfmerge(gs("foo"), new GlideString[] {key1}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + executionException = + assertThrows( + ExecutionException.class, + () -> client.pfmerge(key1, new GlideString[] {gs("foo")}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_null(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + assertNull(client.objectEncoding(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_binary_returns_null(BaseClient client) { + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + assertNull(client.objectEncoding(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_string_raw(BaseClient client) { + String stringRawKey = UUID.randomUUID().toString(); + assertEquals( + OK, + client + .set(stringRawKey, "a really loooooooooooooooooooooooooooooooooooooooong value") + .get()); + assertEquals("raw", client.objectEncoding(stringRawKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_binary_returns_string_raw(BaseClient client) { + GlideString stringRawKey = gs(UUID.randomUUID().toString()); + assertEquals( + OK, + client + .set(stringRawKey, gs("a really loooooooooooooooooooooooooooooooooooooooong value")) + .get()); + assertEquals("raw", client.objectEncoding(stringRawKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_string_int(BaseClient client) { + String stringIntKey = UUID.randomUUID().toString(); + assertEquals(OK, client.set(stringIntKey, "2").get()); + assertEquals("int", client.objectEncoding(stringIntKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_binary_returns_string_int(BaseClient client) { + GlideString stringIntKey = gs(UUID.randomUUID().toString()); + assertEquals(OK, client.set(stringIntKey, gs("2")).get()); + assertEquals("int", client.objectEncoding(stringIntKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_string_embstr(BaseClient client) { + String stringEmbstrKey = UUID.randomUUID().toString(); + assertEquals(OK, client.set(stringEmbstrKey, "value").get()); + assertEquals("embstr", client.objectEncoding(stringEmbstrKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_binary_returns_string_embstr(BaseClient client) { + GlideString stringEmbstrKey = gs(UUID.randomUUID().toString()); + assertEquals(OK, client.set(stringEmbstrKey, gs("value")).get()); + assertEquals("embstr", client.objectEncoding(stringEmbstrKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_list_listpack(BaseClient client) { + String listListpackKey = UUID.randomUUID().toString(); + assertEquals(1, client.lpush(listListpackKey, new String[] {"1"}).get()); + // API documentation states that a ziplist should be returned for Valkey versions < 7.2, but + // actual behavior returns a quicklist. + assertEquals( + SERVER_VERSION.isLowerThan("7.2.0") ? "quicklist" : "listpack", + client.objectEncoding(listListpackKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_binary_returns_list_listpack(BaseClient client) { + GlideString listListpackKey = gs(UUID.randomUUID().toString()); + assertEquals(1, client.lpush(listListpackKey, new GlideString[] {gs("1")}).get()); + // API documentation states that a ziplist should be returned for Valkey versions <= 6.2, but + // actual behavior returns a quicklist. + assertEquals( + SERVER_VERSION.isLowerThan("7.2.0") ? "quicklist" : "listpack", + client.objectEncoding(listListpackKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_set_hashtable(BaseClient client) { + String setHashtableKey = UUID.randomUUID().toString(); + // The default value of set-max-intset-entries is 512 + for (Integer i = 0; i <= 512; i++) { + assertEquals(1, client.sadd(setHashtableKey, new String[] {i.toString()}).get()); + } + assertEquals("hashtable", client.objectEncoding(setHashtableKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_set_intset(BaseClient client) { + String setIntsetKey = UUID.randomUUID().toString(); + assertEquals(1, client.sadd(setIntsetKey, new String[] {"1"}).get()); + assertEquals("intset", client.objectEncoding(setIntsetKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_set_listpack(BaseClient client) { + String setListpackKey = UUID.randomUUID().toString(); + assertEquals(1, client.sadd(setListpackKey, new String[] {"foo"}).get()); + assertEquals( + SERVER_VERSION.isLowerThan("7.2.0") ? "hashtable" : "listpack", + client.objectEncoding(setListpackKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_hash_hashtable(BaseClient client) { + String hashHashtableKey = UUID.randomUUID().toString(); + // The default value of hash-max-listpack-entries is 512 + for (Integer i = 0; i <= 512; i++) { + assertEquals(1, client.hset(hashHashtableKey, Map.of(i.toString(), "2")).get()); + } + assertEquals("hashtable", client.objectEncoding(hashHashtableKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_hash_listpack(BaseClient client) { + String hashListpackKey = UUID.randomUUID().toString(); + assertEquals(1, client.hset(hashListpackKey, Map.of("1", "2")).get()); + assertEquals( + SERVER_VERSION.isLowerThan("7.0.0") ? "ziplist" : "listpack", + client.objectEncoding(hashListpackKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_zset_skiplist(BaseClient client) { + String zsetSkiplistKey = UUID.randomUUID().toString(); + // The default value of zset-max-listpack-entries is 128 + for (Integer i = 0; i <= 128; i++) { + assertEquals(1, client.zadd(zsetSkiplistKey, Map.of(i.toString(), 2d)).get()); + } + assertEquals("skiplist", client.objectEncoding(zsetSkiplistKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_zset_listpack(BaseClient client) { + String zsetListpackKey = UUID.randomUUID().toString(); + assertEquals(1, client.zadd(zsetListpackKey, Map.of("1", 2d)).get()); + assertEquals( + SERVER_VERSION.isLowerThan("7.0.0") ? "ziplist" : "listpack", + client.objectEncoding(zsetListpackKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectEncoding_returns_stream(BaseClient client) { + String streamKey = UUID.randomUUID().toString(); + assertNotNull(client.xadd(streamKey, Map.of("field", "value"))); + assertEquals("stream", client.objectEncoding(streamKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectFreq_returns_null(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + assertNull(client.objectFreq(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectFreq_binary_returns_null(BaseClient client) { + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + assertNull(client.objectFreq(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectIdletime_returns_null(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + assertNull(client.objectIdletime(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectIdletime_binary_returns_null(BaseClient client) { + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + assertNull(client.objectIdletime(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectIdletime(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "").get()); + Thread.sleep(2000); + assertTrue(client.objectIdletime(key).get() > 0L); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectIdletime_binary_(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + assertEquals(OK, client.set(key, gs("")).get()); + Thread.sleep(2000); + assertTrue(client.objectIdletime(key).get() > 0L); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectRefcount_returns_null(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + assertNull(client.objectRefcount(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectRefcount_binary_returns_null(BaseClient client) { + String nonExistingKey = UUID.randomUUID().toString(); + assertNull(client.objectRefcount(nonExistingKey).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectRefcount(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "").get()); + assertTrue(client.objectRefcount(key).get() >= 0L); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void objectRefcount_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + assertEquals(OK, client.set(key, gs("")).get()); + assertTrue(client.objectRefcount(key).get() >= 0L); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void touch(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String key3 = UUID.randomUUID().toString(); + String value = "{value}" + UUID.randomUUID(); + + assertEquals(OK, client.set(key1, value).get()); + assertEquals(OK, client.set(key2, value).get()); + + assertEquals(2, client.touch(new String[] {key1, key2}).get()); + assertEquals(0, client.touch(new String[] {key3}).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void touch_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString key3 = gs(UUID.randomUUID().toString()); + GlideString value = gs("{value}" + UUID.randomUUID()); + + assertEquals(OK, client.set(key1, value).get()); + assertEquals(OK, client.set(key2, value).get()); + + assertEquals(2, client.touch(new GlideString[] {key1, key2}).get()); + assertEquals(0, client.touch(new GlideString[] {key3}).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geoadd(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put("Palermo", new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put("Catania", new GeospatialData(15.087269, 37.502669)); + + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + membersToCoordinates.put("Catania", new GeospatialData(15.087269, 39)); + assertEquals( + 0, + client + .geoadd( + key1, + membersToCoordinates, + new GeoAddOptions(ConditionalChange.ONLY_IF_DOES_NOT_EXIST)) + .get()); + assertEquals( + 0, + client + .geoadd(key1, membersToCoordinates, new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS)) + .get()); + + membersToCoordinates.put("Catania", new GeospatialData(15.087269, 40)); + membersToCoordinates.put("Tel-Aviv", new GeospatialData(32.0853, 34.7818)); + assertEquals(2, client.geoadd(key1, membersToCoordinates, new GeoAddOptions(true)).get()); + + assertEquals(OK, client.set(key2, "bar").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.geoadd(key2, membersToCoordinates).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geoadd_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put(gs("Palermo"), new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 37.502669)); + + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 39)); + assertEquals( + 0, + client + .geoadd( + key1, + membersToCoordinates, + new GeoAddOptions(ConditionalChange.ONLY_IF_DOES_NOT_EXIST)) + .get()); + assertEquals( + 0, + client + .geoadd(key1, membersToCoordinates, new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS)) + .get()); + + membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 40)); + membersToCoordinates.put(gs("Tel-Aviv"), new GeospatialData(32.0853, 34.7818)); + assertEquals(2, client.geoadd(key1, membersToCoordinates, new GeoAddOptions(true)).get()); + + assertEquals(OK, client.set(key2, gs("bar")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, () -> client.geoadd(key2, membersToCoordinates).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geoadd_invalid_args(BaseClient client) { + String key = UUID.randomUUID().toString(); + + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geoadd(key, Map.of()).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of("Place", new GeospatialData(-181, 0))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of("Place", new GeospatialData(181, 0))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of("Place", new GeospatialData(0, 86))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of("Place", new GeospatialData(0, -86))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geoadd_binary_invalid_args(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geoadd(key, Map.of()).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of(gs("Place"), new GeospatialData(-181, 0))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of(gs("Place"), new GeospatialData(181, 0))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of(gs("Place"), new GeospatialData(0, 86))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.geoadd(key, Map.of(gs("Place"), new GeospatialData(0, -86))).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geopos(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String[] members = {"Palermo", "Catania"}; + Double[][] expected = { + {13.36138933897018433, 38.11555639549629859}, {15.08726745843887329, 37.50266842333162032} + }; + + // adding locations + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put("Palermo", new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put("Catania", new GeospatialData(15.087269, 37.502669)); + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + // Loop through the arrays and perform assertions + Double[][] actual = client.geopos(key1, members).get(); + for (int i = 0; i < expected.length; i++) { + for (int j = 0; j < expected[i].length; j++) { + assertEquals(expected[i][j], actual[i][j], 1e-9); + } + } + + // key exists but holding the wrong kind of value (non-ZSET) + assertEquals(OK, client.set(key2, "geopos").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geopos(key2, members).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geopos_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString[] members = {gs("Palermo"), gs("Catania")}; + Double[][] expected = { + {13.36138933897018433, 38.11555639549629859}, {15.08726745843887329, 37.50266842333162032} + }; + + // adding locations + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put(gs("Palermo"), new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 37.502669)); + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + // Loop through the arrays and perform assertions + Double[][] actual = client.geopos(key1, members).get(); + for (int i = 0; i < expected.length; i++) { + for (int j = 0; j < expected[i].length; j++) { + assertEquals(expected[i][j], actual[i][j], 1e-9); + } + } + + // key exists but holding the wrong kind of value (non-ZSET) + assertEquals(OK, client.set(key2, gs("geopos")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geopos(key2, members).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geodist(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String member1 = "Palermo"; + String member2 = "Catania"; + String member3 = "NonExisting"; + GeoUnit geoUnitKM = GeoUnit.KILOMETERS; + Double expected = 166274.1516; + Double expectedKM = 166.2742; + Double delta = 1e-9; + + // adding locations + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put("Palermo", new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put("Catania", new GeospatialData(15.087269, 37.502669)); + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + // assert correct result with default metric + Double actual = client.geodist(gs(key1), gs(member1), gs(member2)).get(); + assertEquals(expected, actual, delta); + + // assert correct result with manual metric specification kilometers + Double actualKM = client.geodist(gs(key1), gs(member1), gs(member2), geoUnitKM).get(); + assertEquals(expectedKM, actualKM, delta); + + // assert null result when member index is missing + Double actualMissing = client.geodist(key1, member1, member3).get(); + assertNull(actualMissing); + + // key exists but holds a non-ZSET value + assertEquals(OK, client.set(key2, "geodist").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geodist(key2, member1, member2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geodist_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString member1 = gs("Palermo"); + GlideString member2 = gs("Catania"); + GlideString member3 = gs("NonExisting"); + GeoUnit geoUnitKM = GeoUnit.KILOMETERS; + Double expected = 166274.1516; + Double expectedKM = 166.2742; + Double delta = 1e-9; + + // adding locations + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put(gs("Palermo"), new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 37.502669)); + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + // assert correct result with default metric + Double actual = client.geodist(key1, member1, member2).get(); + assertEquals(expected, actual, delta); + + // assert correct result with manual metric specification kilometers + Double actualKM = client.geodist(key1, member1, member2, geoUnitKM).get(); + assertEquals(expectedKM, actualKM, delta); + + // assert null result when member index is missing + Double actualMissing = client.geodist(key1, member1, member3).get(); + assertNull(actualMissing); + + // key exists but holds a non-ZSET value + assertEquals(OK, client.set(key2, gs("geodist")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geodist(key2, member1, member2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geohash(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String[] members = {"Palermo", "Catania", "NonExisting"}; + String[] empty = {}; + String[] expected = {"sqc8b49rny0", "sqdtr74hyu0", null}; + + // adding locations + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put("Palermo", new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put("Catania", new GeospatialData(15.087269, 37.502669)); + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + String[] actual = client.geohash(key1, members).get(); + assertArrayEquals(expected, actual); + + // members array is empty + assertEquals(client.geohash(key1, empty).get().length, 0); + + // key exists but holding the wrong kind of value (non-ZSET) + assertEquals(OK, client.set(key2, "geohash").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geohash(key2, members).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geohash_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString[] members = {gs("Palermo"), gs("Catania"), gs("NonExisting")}; + GlideString[] empty = {}; + GlideString[] expected = {gs("sqc8b49rny0"), gs("sqdtr74hyu0"), null}; + + // adding locations + Map membersToCoordinates = new HashMap<>(); + membersToCoordinates.put(gs("Palermo"), new GeospatialData(13.361389, 38.115556)); + membersToCoordinates.put(gs("Catania"), new GeospatialData(15.087269, 37.502669)); + assertEquals(2, client.geoadd(key1, membersToCoordinates).get()); + + GlideString[] actual = client.geohash(key1, members).get(); + assertArrayEquals(expected, actual); + + // members array is empty + assertEquals(client.geohash(key1, empty).get().length, 0); + + // key exists but holding the wrong kind of value (non-ZSET) + assertEquals(OK, client.set(key2, gs("geohash")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.geohash(key2, members).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitcount(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String missingKey = "missing"; + String value = "foobar"; + + assertEquals(OK, client.set(key1, value).get()); + assertEquals(1, client.sadd(key2, new String[] {value}).get()); + assertEquals(26, client.bitcount(key1).get()); + assertEquals(6, client.bitcount(key1, 1, 1).get()); + assertEquals(10, client.bitcount(key1, 0, -5).get()); + assertEquals(0, client.bitcount(missingKey, 5, 30).get()); + assertEquals(0, client.bitcount(missingKey).get()); + + // Exception thrown due to the key holding a value with the wrong type + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.bitcount(key2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown due to the key holding a value with the wrong type + executionException = + assertThrows(ExecutionException.class, () -> client.bitcount(key2, 1, 1).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(16L, client.bitcount(key1, 2, 5, BitmapIndexType.BYTE).get()); + assertEquals(17L, client.bitcount(key1, 5, 30, BitmapIndexType.BIT).get()); + assertEquals(23, client.bitcount(key1, 5, -5, BitmapIndexType.BIT).get()); + assertEquals(0, client.bitcount(missingKey, 5, 30, BitmapIndexType.BIT).get()); + + // Exception thrown due to the key holding a value with the wrong type + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitcount(key2, 1, 1, BitmapIndexType.BIT).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } else { + // Exception thrown because BIT and BYTE options were implemented after 7.0.0 + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitcount(key1, 2, 5, BitmapIndexType.BYTE).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown because BIT and BYTE options were implemented after 7.0.0 + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitcount(key1, 5, 30, BitmapIndexType.BIT).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void setbit(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + assertEquals(0, client.setbit(key1, 0, 1).get()); + assertEquals(1, client.setbit(key1, 0, 0).get()); + assertEquals(0, client.setbit(gs(key1), 0, 1).get()); + assertEquals(1, client.setbit(gs(key1), 0, 0).get()); + + // Exception thrown due to the negative offset + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.setbit(key1, -1, 1).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown due to the value set not being 0 or 1 + executionException = + assertThrows(ExecutionException.class, () -> client.setbit(key1, 1, 2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown: key is not a string + assertEquals(1, client.sadd(key2, new String[] {"value"}).get()); + executionException = + assertThrows(ExecutionException.class, () -> client.setbit(key2, 1, 1).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void getbit(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String missingKey = UUID.randomUUID().toString(); + String value = "foobar"; + assertEquals(OK, client.set(key1, value).get()); + assertEquals(1, client.getbit(key1, 1).get()); + assertEquals(0, client.getbit(key1, 1000).get()); + assertEquals(0, client.getbit(missingKey, 1).get()); + assertEquals(1, client.getbit(gs(key1), 1).get()); + assertEquals(0, client.getbit(gs(key1), 1000).get()); + assertEquals(0, client.getbit(gs(missingKey), 1).get()); + if (client instanceof GlideClient) { + assertEquals( + 1L, ((GlideClient) client).customCommand(new String[] {"SETBIT", key1, "5", "0"}).get()); + assertEquals(0, client.getbit(key1, 5).get()); + } + + // Exception thrown due to the negative offset + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.getbit(key1, -1).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(key2, new String[] {value}).get()); + executionException = assertThrows(ExecutionException.class, () -> client.getbit(key2, 1).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitpos(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String key3 = UUID.randomUUID().toString(); + String value = "?f0obar"; // 00111111 01100110 00110000 01101111 01100010 01100001 01110010 + + assertEquals(OK, client.set(key1, value).get()); + assertEquals(0, client.bitpos(key1, 0).get()); + assertEquals(2, client.bitpos(key1, 1).get()); + assertEquals(9, client.bitpos(key1, 1, 1).get()); + assertEquals(24, client.bitpos(key1, 0, 3, 5).get()); + + // Bitpos returns -1 for empty strings + assertEquals(-1, client.bitpos(key2, 1).get()); + assertEquals(-1, client.bitpos(key2, 1, 1).get()); + assertEquals(-1, client.bitpos(key2, 1, 3, 5).get()); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(key3, new String[] {value}).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.bitpos(key3, 0).get()); + assertTrue(executionException.getCause() instanceof RequestException); + executionException = + assertThrows(ExecutionException.class, () -> client.bitpos(key3, 1, 2).get()); + assertTrue(executionException.getCause() instanceof RequestException); + executionException = + assertThrows(ExecutionException.class, () -> client.bitpos(key3, 0, 1, 4).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertEquals(24, client.bitpos(key1, 0, 3, 5, BitmapIndexType.BYTE).get()); + assertEquals(47, client.bitpos(key1, 1, 43, -2, BitmapIndexType.BIT).get()); + assertEquals(-1, client.bitpos(key2, 1, 3, 5, BitmapIndexType.BYTE).get()); + assertEquals(-1, client.bitpos(key2, 1, 3, 5, BitmapIndexType.BIT).get()); + + // Exception thrown due to the key holding a value with the wrong type + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitpos(key3, 1, 4, 5, BitmapIndexType.BIT).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } else { + // Exception thrown because BIT and BYTE options were implemented after 7.0.0 + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitpos(key1, 0, 3, 5, BitmapIndexType.BYTE).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown because BIT and BYTE options were implemented after 7.0.0 + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitpos(key1, 1, 43, -2, BitmapIndexType.BIT).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitop(BaseClient client) { + String key1 = "{key}-1".concat(UUID.randomUUID().toString()); + String key2 = "{key}-2".concat(UUID.randomUUID().toString()); + String emptyKey1 = "{key}-3".concat(UUID.randomUUID().toString()); + String emptyKey2 = "{key}-4".concat(UUID.randomUUID().toString()); + String destination = "{key}-5".concat(UUID.randomUUID().toString()); + String[] keys = new String[] {key1, key2}; + String[] emptyKeys = new String[] {emptyKey1, emptyKey2}; + String value1 = "foobar"; + String value2 = "abcdef"; + + assertEquals(OK, client.set(key1, value1).get()); + assertEquals(OK, client.set(key2, value2).get()); + assertEquals(6L, client.bitop(BitwiseOperation.AND, destination, keys).get()); + assertEquals("`bc`ab", client.get(destination).get()); + assertEquals(6L, client.bitop(BitwiseOperation.OR, destination, keys).get()); + assertEquals("goofev", client.get(destination).get()); + + // Reset values for simplicity of results in XOR + assertEquals(OK, client.set(key1, "a").get()); + assertEquals(OK, client.set(key2, "b").get()); + assertEquals(1L, client.bitop(BitwiseOperation.XOR, destination, keys).get()); + assertEquals("\u0003", client.get(destination).get()); + + // Test single source key + assertEquals(1L, client.bitop(BitwiseOperation.AND, destination, new String[] {key1}).get()); + assertEquals("a", client.get(destination).get()); + assertEquals(1L, client.bitop(BitwiseOperation.OR, destination, new String[] {key1}).get()); + assertEquals("a", client.get(destination).get()); + assertEquals(1L, client.bitop(BitwiseOperation.XOR, destination, new String[] {key1}).get()); + assertEquals("a", client.get(destination).get()); + assertEquals(1L, client.bitop(BitwiseOperation.NOT, destination, new String[] {key1}).get()); + // First bit is flipped to 1 and throws 'utf-8' codec can't decode byte 0x9e in position 0: + // invalid start byte + // TODO: update once fix is implemented for + // https://github.com/valkey-io/valkey-glide/issues/1447 + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.get(destination).get()); + assertTrue(executionException.getCause() instanceof RuntimeException); + assertEquals(0, client.setbit(key1, 0, 1).get()); + assertEquals(1L, client.bitop(BitwiseOperation.NOT, destination, new String[] {key1}).get()); + assertEquals("\u001e", client.get(destination).get()); + + // Returns null when all keys hold empty strings + assertEquals(0L, client.bitop(BitwiseOperation.AND, destination, emptyKeys).get()); + assertNull(client.get(destination).get()); + assertEquals(0L, client.bitop(BitwiseOperation.OR, destination, emptyKeys).get()); + assertNull(client.get(destination).get()); + assertEquals(0L, client.bitop(BitwiseOperation.XOR, destination, emptyKeys).get()); + assertNull(client.get(destination).get()); + assertEquals( + 0L, client.bitop(BitwiseOperation.NOT, destination, new String[] {emptyKey1}).get()); + assertNull(client.get(destination).get()); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(emptyKey1, new String[] {value1}).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitop(BitwiseOperation.AND, destination, new String[] {emptyKey1}).get()); + + // Source keys is an empty list + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitop(BitwiseOperation.OR, destination, new String[] {}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // NOT with more than one source key + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitop(BitwiseOperation.NOT, destination, new String[] {key1, key2}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lmpop(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonListKey = "{key}-3" + UUID.randomUUID(); + String[] singleKeyArray = {key1}; + String[] multiKeyArray = {key2, key1}; + long count = 1L; + Long arraySize = 5L; + String[] lpushArgs = {"one", "two", "three", "four", "five"}; + Map expected = Map.of(key1, new String[] {"five"}); + Map expected2 = Map.of(key2, new String[] {"one", "two"}); + + // nothing to be popped + assertNull(client.lmpop(singleKeyArray, ListDirection.LEFT).get()); + assertNull(client.lmpop(singleKeyArray, ListDirection.LEFT, count).get()); + + // pushing to the arrays to be popped + assertEquals(arraySize, client.lpush(key1, lpushArgs).get()); + assertEquals(arraySize, client.lpush(key2, lpushArgs).get()); + + // assert correct result from popping + Map result = client.lmpop(singleKeyArray, ListDirection.LEFT).get(); + assertDeepEquals(result, expected); + + // assert popping multiple elements from the right + Map result2 = client.lmpop(multiKeyArray, ListDirection.RIGHT, 2L).get(); + assertDeepEquals(result2, expected2); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonListKey, "lmpop").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.lmpop(new String[] {nonListKey}, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lmpop_binary(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonListKey = gs("{key}-3" + UUID.randomUUID()); + GlideString[] singleKeyArray = {key1}; + GlideString[] multiKeyArray = {key2, key1}; + long count = 1L; + Long arraySize = 5L; + GlideString[] lpushArgs = {gs("one"), gs("two"), gs("three"), gs("four"), gs("five")}; + Map expected = Map.of(key1, new GlideString[] {gs("five")}); + Map expected2 = + Map.of(key2, new GlideString[] {gs("one"), gs("two")}); + + // nothing to be popped + assertNull(client.lmpop(singleKeyArray, ListDirection.LEFT).get()); + assertNull(client.lmpop(singleKeyArray, ListDirection.LEFT, count).get()); + + // pushing to the arrays to be popped + assertEquals(arraySize, client.lpush(key1, lpushArgs).get()); + assertEquals(arraySize, client.lpush(key2, lpushArgs).get()); + + // assert correct result from popping + Map result = client.lmpop(singleKeyArray, ListDirection.LEFT).get(); + assertDeepEquals(result, expected); + + // assert popping multiple elements from the right + Map result2 = + client.lmpop(multiKeyArray, ListDirection.RIGHT, 2L).get(); + assertDeepEquals(result2, expected2); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonListKey, gs("lmpop")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.lmpop(new GlideString[] {nonListKey}, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmpop(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonListKey = "{key}-3" + UUID.randomUUID(); + String[] singleKeyArray = {key1}; + String[] multiKeyArray = {key2, key1}; + long count = 1L; + Long arraySize = 5L; + String[] lpushArgs = {"one", "two", "three", "four", "five"}; + Map expected = Map.of(key1, new String[] {"five"}); + Map expected2 = Map.of(key2, new String[] {"one", "two"}); + + // nothing to be popped + assertNull(client.blmpop(singleKeyArray, ListDirection.LEFT, 0.1).get()); + assertNull(client.blmpop(singleKeyArray, ListDirection.LEFT, count, 0.1).get()); + + // pushing to the arrays to be popped + assertEquals(arraySize, client.lpush(key1, lpushArgs).get()); + assertEquals(arraySize, client.lpush(key2, lpushArgs).get()); + + // assert correct result from popping + Map result = client.blmpop(singleKeyArray, ListDirection.LEFT, 0.1).get(); + assertDeepEquals(result, expected); + + // assert popping multiple elements from the right + Map result2 = + client.blmpop(multiKeyArray, ListDirection.RIGHT, 2L, 0.1).get(); + assertDeepEquals(result2, expected2); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonListKey, "blmpop").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.blmpop(new String[] {nonListKey}, ListDirection.LEFT, 0.1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmpop_binary(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonListKey = gs("{key}-3" + UUID.randomUUID()); + GlideString[] singleKeyArray = {key1}; + GlideString[] multiKeyArray = {key2, key1}; + long count = 1L; + Long arraySize = 5L; + GlideString[] lpushArgs = {gs("one"), gs("two"), gs("three"), gs("four"), gs("five")}; + Map expected = Map.of(key1, new GlideString[] {gs("five")}); + Map expected2 = + Map.of(key2, new GlideString[] {gs("one"), gs("two")}); + + // nothing to be popped + assertNull(client.blmpop(singleKeyArray, ListDirection.LEFT, 0.1).get()); + assertNull(client.blmpop(singleKeyArray, ListDirection.LEFT, count, 0.1).get()); + + // pushing to the arrays to be popped + assertEquals(arraySize, client.lpush(key1, lpushArgs).get()); + assertEquals(arraySize, client.lpush(key2, lpushArgs).get()); + + // assert correct result from popping + Map result = + client.blmpop(singleKeyArray, ListDirection.LEFT, 0.1).get(); + assertDeepEquals(result, expected); + + // assert popping multiple elements from the right + Map result2 = + client.blmpop(multiKeyArray, ListDirection.RIGHT, 2L, 0.1).get(); + assertDeepEquals(result2, expected2); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonListKey, gs("blmpop")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.blmpop(new GlideString[] {nonListKey}, ListDirection.LEFT, 0.1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmpop_timeout_check(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + String key = UUID.randomUUID().toString(); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.blmpop(new String[] {key}, ListDirection.LEFT, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .blmpop(new String[] {key}, ListDirection.LEFT, 0) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmpop_binary_timeout_check(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + GlideString key = gs(UUID.randomUUID().toString()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.blmpop(new GlideString[] {key}, ListDirection.LEFT, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .blmpop(new GlideString[] {key}, ListDirection.LEFT, 0) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lset(BaseClient client) { + // setup + String key = UUID.randomUUID().toString(); + String nonExistingKey = UUID.randomUUID().toString(); + long index = 0; + long oobIndex = 10; + long negativeIndex = -1; + String element = "zero"; + String[] lpushArgs = {"four", "three", "two", "one"}; + String[] expectedList = {"zero", "two", "three", "four"}; + String[] expectedList2 = {"zero", "two", "three", "zero"}; + + // key does not exist + ExecutionException noSuchKeyException = + assertThrows( + ExecutionException.class, () -> client.lset(nonExistingKey, index, element).get()); + assertInstanceOf(RequestException.class, noSuchKeyException.getCause()); + + // pushing elements to list + client.lpush(key, lpushArgs).get(); + + // index out of range + ExecutionException indexOutOfBoundException = + assertThrows(ExecutionException.class, () -> client.lset(key, oobIndex, element).get()); + assertInstanceOf(RequestException.class, indexOutOfBoundException.getCause()); + + // assert lset result + String response = client.lset(key, index, element).get(); + assertEquals(OK, response); + String[] updatedList = client.lrange(key, 0, -1).get(); + assertArrayEquals(updatedList, expectedList); + + // assert lset with a negative index for the last element in the list + String response2 = client.lset(key, negativeIndex, element).get(); + assertEquals(OK, response2); + String[] updatedList2 = client.lrange(key, 0, -1).get(); + assertArrayEquals(updatedList2, expectedList2); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lset_binary(BaseClient client) { + // setup + GlideString key = gs(UUID.randomUUID().toString()); + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + long index = 0; + long oobIndex = 10; + long negativeIndex = -1; + GlideString element = gs("zero"); + GlideString[] lpushArgs = {gs("four"), gs("three"), gs("two"), gs("one")}; + String[] expectedList = {"zero", "two", "three", "four"}; + String[] expectedList2 = {"zero", "two", "three", "zero"}; + + // key does not exist + ExecutionException noSuchKeyException = + assertThrows( + ExecutionException.class, () -> client.lset(nonExistingKey, index, element).get()); + assertInstanceOf(RequestException.class, noSuchKeyException.getCause()); + + // pushing elements to list + client.lpush(key, lpushArgs).get(); + + // index out of range + ExecutionException indexOutOfBoundException = + assertThrows(ExecutionException.class, () -> client.lset(key, oobIndex, element).get()); + assertInstanceOf(RequestException.class, indexOutOfBoundException.getCause()); + + // assert lset result + String response = client.lset(key, index, element).get(); + assertEquals(OK, response); + String[] updatedList = client.lrange(key.toString(), 0, -1).get(); + assertArrayEquals(updatedList, expectedList); + + // assert lset with a negative index for the last element in the list + String response2 = client.lset(key, negativeIndex, element).get(); + assertEquals(OK, response2); + String[] updatedList2 = client.lrange(key.toString(), 0, -1).get(); + assertArrayEquals(updatedList2, expectedList2); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lmove(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonExistingKey = "{key}-3" + UUID.randomUUID(); + String nonListKey = "{key}-4" + UUID.randomUUID(); + String[] lpushArgs1 = {"four", "three", "two", "one"}; + String[] lpushArgs2 = {"six", "five", "four"}; + + // source does not exist or is empty + assertNull(client.lmove(key1, key2, ListDirection.LEFT, ListDirection.RIGHT).get()); + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + assertEquals(lpushArgs1.length, client.lpush(key1, lpushArgs1).get()); + assertEquals( + "four", client.lmove(key1, nonExistingKey, ListDirection.RIGHT, ListDirection.LEFT).get()); + assertArrayEquals(new String[] {"one", "two", "three"}, client.lrange(key1, 0, -1).get()); + + // source and destination are the same, performing list rotation, "three" gets popped and added + // back + assertEquals("one", client.lmove(key1, key1, ListDirection.LEFT, ListDirection.LEFT).get()); + assertArrayEquals(new String[] {"one", "two", "three"}, client.lrange(key1, 0, -1).get()); + + // normal use case, "three" gets popped and added to the left of destination + assertEquals(lpushArgs2.length, client.lpush(key2, lpushArgs2).get()); + assertEquals("three", client.lmove(key1, key2, ListDirection.RIGHT, ListDirection.LEFT).get()); + assertArrayEquals(new String[] {"one", "two"}, client.lrange(key1, 0, -1).get()); + assertArrayEquals( + new String[] {"three", "four", "five", "six"}, client.lrange(key2, 0, -1).get()); + + // source exists but is not a list type key + assertEquals(OK, client.set(nonListKey, "NotAList").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.lmove(nonListKey, key1, ListDirection.LEFT, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // destination exists but is not a list type key + ExecutionException executionException2 = + assertThrows( + ExecutionException.class, + () -> client.lmove(key1, nonListKey, ListDirection.LEFT, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lmove_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonExistingKey = gs("{key}-3" + UUID.randomUUID()); + GlideString nonListKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] lpushArgs1 = {gs("four"), gs("three"), gs("two"), gs("one")}; + GlideString[] lpushArgs2 = {gs("six"), gs("five"), gs("four")}; + + // source does not exist or is empty + assertNull(client.lmove(key1, key2, ListDirection.LEFT, ListDirection.RIGHT).get()); + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + assertEquals(lpushArgs1.length, client.lpush(key1, lpushArgs1).get()); + assertEquals( + gs("four"), + client.lmove(key1, nonExistingKey, ListDirection.RIGHT, ListDirection.LEFT).get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // source and destination are the same, performing list rotation, "three" gets popped and added + // back + assertEquals(gs("one"), client.lmove(key1, key1, ListDirection.LEFT, ListDirection.LEFT).get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // normal use case, "three" gets popped and added to the left of destination + assertEquals(lpushArgs2.length, client.lpush(key2, lpushArgs2).get()); + assertEquals( + gs("three"), client.lmove(key1, key2, ListDirection.RIGHT, ListDirection.LEFT).get()); + assertArrayEquals(new String[] {"one", "two"}, client.lrange(key1.toString(), 0, -1).get()); + assertArrayEquals( + new String[] {"three", "four", "five", "six"}, client.lrange(key2.toString(), 0, -1).get()); + + // source exists but is not a list type key + assertEquals(OK, client.set(nonListKey, gs("NotAList")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.lmove(nonListKey, key1, ListDirection.LEFT, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // destination exists but is not a list type key + ExecutionException executionException2 = + assertThrows( + ExecutionException.class, + () -> client.lmove(key1, nonListKey, ListDirection.LEFT, ListDirection.LEFT).get()); + assertInstanceOf(RequestException.class, executionException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmove(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonExistingKey = "{key}-3" + UUID.randomUUID(); + String nonListKey = "{key}-4" + UUID.randomUUID(); + String[] lpushArgs1 = {"four", "three", "two", "one"}; + String[] lpushArgs2 = {"six", "five", "four"}; + double timeout = 1; + + // source does not exist or is empty + assertNull(client.blmove(key1, key2, ListDirection.LEFT, ListDirection.RIGHT, timeout).get()); + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + assertEquals(lpushArgs1.length, client.lpush(key1, lpushArgs1).get()); + assertEquals( + "four", + client + .blmove(key1, nonExistingKey, ListDirection.RIGHT, ListDirection.LEFT, timeout) + .get()); + assertArrayEquals(new String[] {"one", "two", "three"}, client.lrange(key1, 0, -1).get()); + + // source and destination are the same, performing list rotation, "three" gets popped and added + // back + assertEquals( + "one", client.blmove(key1, key1, ListDirection.LEFT, ListDirection.LEFT, timeout).get()); + assertArrayEquals(new String[] {"one", "two", "three"}, client.lrange(key1, 0, -1).get()); + + // normal use case, "three" gets popped and added to the left of destination + assertEquals(lpushArgs2.length, client.lpush(key2, lpushArgs2).get()); + assertEquals( + "three", client.blmove(key1, key2, ListDirection.RIGHT, ListDirection.LEFT, timeout).get()); + assertArrayEquals(new String[] {"one", "two"}, client.lrange(key1, 0, -1).get()); + assertArrayEquals( + new String[] {"three", "four", "five", "six"}, client.lrange(key2, 0, -1).get()); + + // source exists but is not a list type key + assertEquals(OK, client.set(nonListKey, "NotAList").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .blmove(nonListKey, key1, ListDirection.LEFT, ListDirection.LEFT, timeout) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // destination exists but is not a list type key + ExecutionException executionException2 = + assertThrows( + ExecutionException.class, + () -> + client + .blmove(key1, nonListKey, ListDirection.LEFT, ListDirection.LEFT, timeout) + .get()); + assertInstanceOf(RequestException.class, executionException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmove_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonExistingKey = gs("{key}-3" + UUID.randomUUID()); + GlideString nonListKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] lpushArgs1 = {gs("four"), gs("three"), gs("two"), gs("one")}; + GlideString[] lpushArgs2 = {gs("six"), gs("five"), gs("four")}; + double timeout = 1; + + // source does not exist or is empty + assertNull(client.blmove(key1, key2, ListDirection.LEFT, ListDirection.RIGHT, timeout).get()); + + // only source exists, only source elements gets popped, creates a list at nonExistingKey + assertEquals(lpushArgs1.length, client.lpush(key1, lpushArgs1).get()); + assertEquals( + gs("four"), + client + .blmove(key1, nonExistingKey, ListDirection.RIGHT, ListDirection.LEFT, timeout) + .get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // source and destination are the same, performing list rotation, "three" gets popped and added + // back + assertEquals( + gs("one"), + client.blmove(key1, key1, ListDirection.LEFT, ListDirection.LEFT, timeout).get()); + assertArrayEquals( + new String[] {"one", "two", "three"}, client.lrange(key1.toString(), 0, -1).get()); + + // normal use case, "three" gets popped and added to the left of destination + assertEquals(lpushArgs2.length, client.lpush(key2, lpushArgs2).get()); + assertEquals( + gs("three"), + client.blmove(key1, key2, ListDirection.RIGHT, ListDirection.LEFT, timeout).get()); + assertArrayEquals(new String[] {"one", "two"}, client.lrange(key1.toString(), 0, -1).get()); + assertArrayEquals( + new String[] {"three", "four", "five", "six"}, client.lrange(key2.toString(), 0, -1).get()); + + // source exists but is not a list type key + assertEquals(OK, client.set(nonListKey, gs("NotAList")).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .blmove(nonListKey, key1, ListDirection.LEFT, ListDirection.LEFT, timeout) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // destination exists but is not a list type key + ExecutionException executionException2 = + assertThrows( + ExecutionException.class, + () -> + client + .blmove(key1, nonListKey, ListDirection.LEFT, ListDirection.LEFT, timeout) + .get()); + assertInstanceOf(RequestException.class, executionException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmove_timeout_check(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.blmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .blmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void blmove_binary_timeout_check(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands doesn't time out even if timeout > request timeout + assertNull(testClient.blmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT, 1).get()); + + // with 0 timeout (no timeout) should never time out, + // but we wrap the test with timeout to avoid test failing or stuck forever + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> + testClient + .blmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0) + .get(3, TimeUnit.SECONDS)); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void srandmember(BaseClient client) { + // setup + String key = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String nonExistingKey = "nonExisting"; + String nonSetKey = "NonSet"; + long count = 2; + long countNegative = -2; + String[] singleArr = new String[] {"one"}; + + // expected results + String expectedNoCount = "one"; + String[] expectedNegCount = new String[] {"one", "one"}; + + // key does not exist, without count the command returns null, and with count command returns an + // empty array + assertNull(client.srandmember(nonExistingKey).get()); + assertEquals(0, client.srandmember(nonExistingKey, count).get().length); + + // adding element to set + client.sadd(key, singleArr).get(); + + // with no count or a positive count, single array result should only contain element "one" + String resultNoCount = client.srandmember(key).get(); + assertEquals(resultNoCount, expectedNoCount); + String[] resultPosCount = client.srandmember(key, count).get(); + assertArrayEquals(resultPosCount, singleArr); + + // with negative count, the same element can be returned multiple times + String[] resultNegCount = client.srandmember(key, countNegative).get(); + assertArrayEquals(resultNegCount, expectedNegCount); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonSetKey, "notaset").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.srandmember(nonSetKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + ExecutionException executionExceptionWithCount = + assertThrows(ExecutionException.class, () -> client.srandmember(nonSetKey, count).get()); + assertInstanceOf(RequestException.class, executionExceptionWithCount.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void srandmember_binary(BaseClient client) { + // setup + GlideString key = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString nonExistingKey = gs("nonExisting"); + GlideString nonSetKey = gs("NonSet"); + long count = 2; + long countNegative = -2; + GlideString[] singleArr = new GlideString[] {gs("one")}; + + // expected results + GlideString expectedNoCount = gs("one"); + GlideString[] expectedNegCount = new GlideString[] {gs("one"), gs("one")}; + + // key does not exist, without count the command returns null, and with count command returns an + // empty array + assertNull(client.srandmember(nonExistingKey).get()); + assertEquals(0, client.srandmember(nonExistingKey, count).get().length); + + // adding element to set + client.sadd(key, singleArr).get(); + + // with no count or a positive count, single array result should only contain element "one" + GlideString resultNoCount = client.srandmember(key).get(); + assertEquals(resultNoCount, expectedNoCount); + GlideString[] resultPosCount = client.srandmember(key, count).get(); + assertArrayEquals(resultPosCount, singleArr); + + // with negative count, the same element can be returned multiple times + GlideString[] resultNegCount = client.srandmember(key, countNegative).get(); + assertArrayEquals(resultNegCount, expectedNegCount); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonSetKey, gs("notaset")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.srandmember(nonSetKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + ExecutionException executionExceptionWithCount = + assertThrows(ExecutionException.class, () -> client.srandmember(nonSetKey, count).get()); + assertInstanceOf(RequestException.class, executionExceptionWithCount.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void spop_spopCount(BaseClient client) { + String key = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String nonExistingKey = UUID.randomUUID().toString(); + String member1 = UUID.randomUUID().toString(); + String member2 = UUID.randomUUID().toString(); + String member3 = UUID.randomUUID().toString(); + + assertEquals(1, client.sadd(key, new String[] {member1}).get()); + assertEquals(member1, client.spop(key).get()); + + assertEquals(3, client.sadd(key, new String[] {member1, member2, member3}).get()); + // Pop with count value greater than the size of the set + assertEquals(Set.of(member1, member2, member3), client.spopCount(key, 4).get()); + assertEquals(0, client.scard(key).get()); + + assertEquals(3, client.sadd(key, new String[] {member1, member2, member3}).get()); + assertEquals(Set.of(), client.spopCount(key, 0).get()); + + assertNull(client.spop(nonExistingKey).get()); + assertEquals(Set.of(), client.spopCount(nonExistingKey, 3).get()); + + // invalid argument - count must be positive + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.spopCount(key, -1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // key exists but is not a set + assertEquals(OK, client.set(stringKey, "foo").get()); + executionException = assertThrows(ExecutionException.class, () -> client.spop(stringKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.spopCount(stringKey, 3).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void spop_spopCount_binary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString stringKey = gs(UUID.randomUUID().toString()); + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + GlideString member1 = gs(UUID.randomUUID().toString()); + GlideString member2 = gs(UUID.randomUUID().toString()); + GlideString member3 = gs(UUID.randomUUID().toString()); + + assertEquals(1, client.sadd(key, new GlideString[] {member1}).get()); + assertEquals(member1, client.spop(key).get()); + + assertEquals(3, client.sadd(key, new GlideString[] {member1, member2, member3}).get()); + // Pop with count value greater than the size of the set + assertEquals(Set.of(member1, member2, member3), client.spopCount(key, 4).get()); + assertEquals(0, client.scard(key).get()); + + assertEquals(3, client.sadd(key, new GlideString[] {member1, member2, member3}).get()); + assertEquals(Set.of(), client.spopCount(key, 0).get()); + + assertNull(client.spop(nonExistingKey).get()); + assertEquals(Set.of(), client.spopCount(nonExistingKey, 3).get()); + + // invalid argument - count must be positive + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.spopCount(key, -1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // key exists but is not a set + assertEquals(OK, client.set(stringKey, gs("foo")).get()); + executionException = assertThrows(ExecutionException.class, () -> client.spop(stringKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.spopCount(stringKey, 3).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitfieldReadOnly(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + BitFieldGet unsignedOffsetGet = new BitFieldGet(new UnsignedEncoding(2), new Offset(1)); + String emptyKey = UUID.randomUUID().toString(); + String foobar = "foobar"; + + client.set(key1, foobar); + assertArrayEquals( + new Long[] {3L, -2L, 118L, 111L}, + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + // Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 + unsignedOffsetGet, + // Get value in: 01100(110) 01101111 01101111 01100010 01100001 01110010 00010100 + new BitFieldGet(new SignedEncoding(3), new Offset(5)), + // Get value in: 01100110 01101111 01101(111 0110)0010 01100001 01110010 00010100 + new BitFieldGet(new UnsignedEncoding(7), new OffsetMultiplier(3)), + // Get value in: 01100110 01101111 (01101111) 01100010 01100001 01110010 00010100 + new BitFieldGet(new SignedEncoding(8), new OffsetMultiplier(2)) + }) + .get()); + assertArrayEquals( + new Long[] {0L}, + client + .bitfieldReadOnly(emptyKey, new BitFieldReadOnlySubCommands[] {unsignedOffsetGet}) + .get()); + + // Empty subcommands return an empty array + assertArrayEquals( + new Long[] {}, client.bitfieldReadOnly(key2, new BitFieldReadOnlySubCommands[] {}).get()); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(key2, new String[] {foobar}).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly(key2, new BitFieldReadOnlySubCommands[] {unsignedOffsetGet}) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Offset must be >= 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(5), new Offset(-1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be > 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(-1), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be < 64 for unsigned bit encoding + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(64), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be < 65 for signed bit encoding + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new SignedEncoding(65), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitfieldReadOnly_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + BitFieldGet unsignedOffsetGet = new BitFieldGet(new UnsignedEncoding(2), new Offset(1)); + GlideString emptyKey = gs(UUID.randomUUID().toString()); + GlideString foobar = gs("foobar"); + + client.set(key1, foobar); + assertArrayEquals( + new Long[] {3L, -2L, 118L, 111L}, + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + // Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 + unsignedOffsetGet, + // Get value in: 01100(110) 01101111 01101111 01100010 01100001 01110010 00010100 + new BitFieldGet(new SignedEncoding(3), new Offset(5)), + // Get value in: 01100110 01101111 01101(111 0110)0010 01100001 01110010 00010100 + new BitFieldGet(new UnsignedEncoding(7), new OffsetMultiplier(3)), + // Get value in: 01100110 01101111 (01101111) 01100010 01100001 01110010 00010100 + new BitFieldGet(new SignedEncoding(8), new OffsetMultiplier(2)) + }) + .get()); + assertArrayEquals( + new Long[] {0L}, + client + .bitfieldReadOnly(emptyKey, new BitFieldReadOnlySubCommands[] {unsignedOffsetGet}) + .get()); + + // Empty subcommands return an empty array + assertArrayEquals( + new Long[] {}, client.bitfieldReadOnly(key2, new BitFieldReadOnlySubCommands[] {}).get()); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(key2, new GlideString[] {foobar}).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly(key2, new BitFieldReadOnlySubCommands[] {unsignedOffsetGet}) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Offset must be >= 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(5), new Offset(-1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be > 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(-1), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be < 64 for unsigned bit encoding + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(64), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be < 65 for signed bit encoding + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new SignedEncoding(65), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitfield(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String setKey = UUID.randomUUID().toString(); + String foobar = "foobar"; + UnsignedEncoding u2 = new UnsignedEncoding(2); + UnsignedEncoding u7 = new UnsignedEncoding(7); + SignedEncoding i3 = new SignedEncoding(3); + SignedEncoding i8 = new SignedEncoding(8); + Offset offset1 = new Offset(1); + Offset offset5 = new Offset(5); + OffsetMultiplier offsetMultiplier4 = new OffsetMultiplier(4); + OffsetMultiplier offsetMultiplier8 = new OffsetMultiplier(8); + BitFieldSet overflowSet = new BitFieldSet(u2, offset1, -10); + BitFieldGet overflowGet = new BitFieldGet(u2, offset1); + + client.set(key1, foobar); // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + + // SET tests + assertArrayEquals( + new Long[] {3L, -2L, 19L, 0L, 2L, 3L, 18L, 20L}, + client + .bitfield( + key1, + new BitFieldSubCommands[] { + // binary value becomes: 0(10)00110 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(u2, offset1, 2), + // binary value becomes: 01000(011) 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(i3, offset5, 3), + // binary value becomes: 01000011 01101111 01101111 0110(0010 010)00001 01110010 + new BitFieldSet(u7, offsetMultiplier4, 18), + // binary value becomes: 01000011 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 (00010100) + new BitFieldSet(i8, offsetMultiplier8, 20), + new BitFieldGet(u2, offset1), + new BitFieldGet(i3, offset5), + new BitFieldGet(u7, offsetMultiplier4), + new BitFieldGet(i8, offsetMultiplier8) + }) + .get()); + + // INCRBY tests + assertArrayEquals( + new Long[] {3L, -3L, 15L, 30L}, + client + .bitfield( + key1, + new BitFieldSubCommands[] { + // binary value becomes: 0(11)00011 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(u2, offset1, 1), + // binary value becomes: 01100(101) 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(i3, offset5, 2), + // binary value becomes: 01100101 01101111 01101111 0110(0001 111)00001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(u7, offsetMultiplier4, -3), + // binary value becomes: 01100101 01101111 01101111 01100001 11100001 01110010 + // 00000000 00000000 (00011110) + new BitFieldIncrby(i8, offsetMultiplier8, 10) + }) + .get()); + + // OVERFLOW WRAP is used by default if no OVERFLOW is specified + assertArrayEquals( + new Long[] {0L, 2L, 2L}, + client + .bitfield( + key2, + new BitFieldSubCommands[] { + overflowSet, + new BitFieldOverflow(BitOverflowControl.WRAP), + overflowSet, + overflowGet + }) + .get()); + + // OVERFLOW affects only SET or INCRBY after OVERFLOW subcommand + assertArrayEquals( + new Long[] {2L, 2L, 3L, null}, + client + .bitfield( + key2, + new BitFieldSubCommands[] { + overflowSet, + new BitFieldOverflow(BitOverflowControl.SAT), + overflowSet, + overflowGet, + new BitFieldOverflow(BitOverflowControl.FAIL), + overflowSet + }) + .get()); + + // Empty subcommands return an empty array + assertArrayEquals(new Long[] {}, client.bitfield(key2, new BitFieldSubCommands[] {}).get()); + + // Exceptions + // Encoding must be > 0 + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldSet(new UnsignedEncoding(-1), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Offset must be > 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldIncrby(new UnsignedEncoding(5), new Offset(-1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Unsigned bit encoding must be < 64 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldIncrby(new UnsignedEncoding(64), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Signed bit encoding must be < 65 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldSet(new SignedEncoding(65), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(setKey, new String[] {foobar}).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + setKey, + new BitFieldSubCommands[] { + new BitFieldSet(new SignedEncoding(3), new Offset(1), 2) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitfield_binary(BaseClient client) { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString setKey = gs(UUID.randomUUID().toString()); + GlideString foobar = gs("foobar"); + UnsignedEncoding u2 = new UnsignedEncoding(2); + UnsignedEncoding u7 = new UnsignedEncoding(7); + SignedEncoding i3 = new SignedEncoding(3); + SignedEncoding i8 = new SignedEncoding(8); + Offset offset1 = new Offset(1); + Offset offset5 = new Offset(5); + OffsetMultiplier offsetMultiplier4 = new OffsetMultiplier(4); + OffsetMultiplier offsetMultiplier8 = new OffsetMultiplier(8); + BitFieldSet overflowSet = new BitFieldSet(u2, offset1, -10); + BitFieldGet overflowGet = new BitFieldGet(u2, offset1); + + client.set(key1, foobar); // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + + // SET tests + assertArrayEquals( + new Long[] {3L, -2L, 19L, 0L, 2L, 3L, 18L, 20L}, + client + .bitfield( + key1, + new BitFieldSubCommands[] { + // binary value becomes: 0(10)00110 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(u2, offset1, 2), + // binary value becomes: 01000(011) 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(i3, offset5, 3), + // binary value becomes: 01000011 01101111 01101111 0110(0010 010)00001 01110010 + new BitFieldSet(u7, offsetMultiplier4, 18), + // binary value becomes: 01000011 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 (00010100) + new BitFieldSet(i8, offsetMultiplier8, 20), + new BitFieldGet(u2, offset1), + new BitFieldGet(i3, offset5), + new BitFieldGet(u7, offsetMultiplier4), + new BitFieldGet(i8, offsetMultiplier8) + }) + .get()); + + // INCRBY tests + assertArrayEquals( + new Long[] {3L, -3L, 15L, 30L}, + client + .bitfield( + key1, + new BitFieldSubCommands[] { + // binary value becomes: 0(11)00011 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(u2, offset1, 1), + // binary value becomes: 01100(101) 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(i3, offset5, 2), + // binary value becomes: 01100101 01101111 01101111 0110(0001 111)00001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(u7, offsetMultiplier4, -3), + // binary value becomes: 01100101 01101111 01101111 01100001 11100001 01110010 + // 00000000 00000000 (00011110) + new BitFieldIncrby(i8, offsetMultiplier8, 10) + }) + .get()); + + // OVERFLOW WRAP is used by default if no OVERFLOW is specified + assertArrayEquals( + new Long[] {0L, 2L, 2L}, + client + .bitfield( + key2, + new BitFieldSubCommands[] { + overflowSet, + new BitFieldOverflow(BitOverflowControl.WRAP), + overflowSet, + overflowGet + }) + .get()); + + // OVERFLOW affects only SET or INCRBY after OVERFLOW subcommand + assertArrayEquals( + new Long[] {2L, 2L, 3L, null}, + client + .bitfield( + key2, + new BitFieldSubCommands[] { + overflowSet, + new BitFieldOverflow(BitOverflowControl.SAT), + overflowSet, + overflowGet, + new BitFieldOverflow(BitOverflowControl.FAIL), + overflowSet + }) + .get()); + + // Empty subcommands return an empty array + assertArrayEquals(new Long[] {}, client.bitfield(key2, new BitFieldSubCommands[] {}).get()); + + // Exceptions + // Encoding must be > 0 + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldSet(new UnsignedEncoding(-1), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Offset must be > 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldIncrby(new UnsignedEncoding(5), new Offset(-1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Unsigned bit encoding must be < 64 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldIncrby(new UnsignedEncoding(64), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Signed bit encoding must be < 65 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldSet(new SignedEncoding(65), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(setKey, new GlideString[] {foobar}).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + setKey, + new BitFieldSubCommands[] { + new BitFieldSet(new SignedEncoding(3), new Offset(1), 2) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sintercard(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonSetKey = "{key}-4" + UUID.randomUUID(); + String[] saddargs = {"one", "two", "three", "four"}; + String[] saddargs2 = {"two", "three", "four", "five"}; + long limit = 2; + long limit2 = 4; + + // keys does not exist or is empty + String[] keys = {key1, key2}; + assertEquals(0, client.sintercard(keys).get()); + assertEquals(0, client.sintercard(keys, limit).get()); + + // one of the keys is empty, intersection is empty, cardinality equals to 0 + assertEquals(4, client.sadd(key1, saddargs).get()); + assertEquals(0, client.sintercard(keys).get()); + + // sets at both keys have value, get cardinality of the intersection + assertEquals(4, client.sadd(key2, saddargs2).get()); + assertEquals(3, client.sintercard(keys).get()); + + // returns limit as cardinality when the limit is reached partway through the computation + assertEquals(limit, client.sintercard(keys, limit).get()); + + // returns actual cardinality if limit is higher + assertEquals(3, client.sintercard(keys, limit2).get()); + + // non set keys are used + assertEquals(OK, client.set(nonSetKey, "NotASet").get()); + String[] badArr = new String[] {key1, nonSetKey}; + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sintercard(badArr).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sintercard_gs(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString nonSetKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] saddargs = {gs("one"), gs("two"), gs("three"), gs("four")}; + GlideString[] saddargs2 = {gs("two"), gs("three"), gs("four"), gs("five")}; + long limit = 2; + long limit2 = 4; + + // keys does not exist or is empty + GlideString[] keys = {key1, key2}; + assertEquals(0, client.sintercard(keys).get()); + assertEquals(0, client.sintercard(keys, limit).get()); + + // one of the keys is empty, intersection is empty, cardinality equals to 0 + assertEquals(4, client.sadd(key1, saddargs).get()); + assertEquals(0, client.sintercard(keys).get()); + + // sets at both keys have value, get cardinality of the intersection + assertEquals(4, client.sadd(key2, saddargs2).get()); + assertEquals(3, client.sintercard(keys).get()); + + // returns limit as cardinality when the limit is reached partway through the computation + assertEquals(limit, client.sintercard(keys, limit).get()); + + // returns actual cardinality if limit is higher + assertEquals(3, client.sintercard(keys, limit2).get()); + + // non set keys are used + assertEquals(OK, client.set(nonSetKey, gs("NotASet")).get()); + GlideString[] badArr = new GlideString[] {key1, nonSetKey}; + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sintercard(badArr).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void copy(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + String source = "{key}-1" + UUID.randomUUID(); + String destination = "{key}-2" + UUID.randomUUID(); + + // neither key exists, returns false + assertFalse(client.copy(source, destination, false).get()); + assertFalse(client.copy(source, destination).get()); + + // source exists, destination does not + client.set(source, "one"); + assertTrue(client.copy(source, destination, false).get()); + assertEquals("one", client.get(destination).get()); + + // setting new value for source + client.set(source, "two"); + + // both exists, no REPLACE + assertFalse(client.copy(source, destination).get()); + assertFalse(client.copy(source, destination, false).get()); + assertEquals("one", client.get(destination).get()); + + // both exists, with REPLACE + assertTrue(client.copy(source, destination, true).get()); + assertEquals("two", client.get(destination).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void copy_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + GlideString source = gs("{key}-1" + UUID.randomUUID()); + GlideString destination = gs("{key}-2" + UUID.randomUUID()); + + // neither key exists, returns false + assertFalse(client.copy(source, destination, false).get()); + assertFalse(client.copy(source, destination).get()); + + // source exists, destination does not + client.set(source, gs("one")); + assertTrue(client.copy(source, destination, false).get()); + assertEquals(gs("one"), client.get(destination).get()); + + // setting new value for source + client.set(source, gs("two")); + + // both exists, no REPLACE + assertFalse(client.copy(source, destination).get()); + assertFalse(client.copy(source, destination, false).get()); + assertEquals(gs("one"), client.get(destination).get()); + + // both exists, with REPLACE + assertTrue(client.copy(source, destination, true).get()); + assertEquals(gs("two"), client.get(destination).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void msetnx(BaseClient client) { + // keys are from different slots + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonExisting = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + Map keyValueMap1 = Map.of(key1, value, key2, value); + Map keyValueMap2 = Map.of(key2, value, key3, value); + + // all keys are empty, successfully set + assertTrue(client.msetnx(keyValueMap1).get()); + assertArrayEquals( + new String[] {value, value, null}, + client.mget(new String[] {key1, key2, nonExisting}).get()); + + // one of the keys is already set, nothing gets set + assertFalse(client.msetnx(keyValueMap2).get()); + assertNull(client.get(key3).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void msetnx_binary(BaseClient client) { + // keys are from different slots + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString nonExisting = gs(UUID.randomUUID().toString()); + GlideString value = gs(UUID.randomUUID().toString()); + Map keyValueMap1 = Map.of(key1, value, key2, value); + Map keyValueMap2 = Map.of(key2, value, key3, value); + + // all keys are empty, successfully set + assertTrue(client.msetnxBinary(keyValueMap1).get()); + assertArrayEquals( + new GlideString[] {value, value, null}, + client.mget(new GlideString[] {key1, key2, nonExisting}).get()); + + // one of the keys is already set, nothing gets set + assertFalse(client.msetnxBinary(keyValueMap2).get()); + assertNull(client.get(key3).get()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcs(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonStringKey = "{key}-4" + UUID.randomUUID(); + + // keys does not exist or is empty + assertEquals("", client.lcs(key1, key2).get()); + + // setting string values + client.set(key1, "abcd"); + client.set(key2, "bcde"); + client.set(key3, "wxyz"); + + // getting the lcs + assertEquals("", client.lcs(key1, key3).get()); + assertEquals("bcd", client.lcs(key1, key2).get()); + + // non set keys are used + client.sadd(nonStringKey, new String[] {"setmember"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcs(nonStringKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcs_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString notExistingKey = gs("{key}-4" + UUID.randomUUID()); + + // keys does not exist or is empty + assertEquals(gs(""), client.lcs(key1, key2).get()); + + // setting string values + client.set(key1, gs("abcd")); + client.set(key2, gs("bcde")); + client.set(key3, gs("wxyz")); + + // getting the lcs + assertEquals(gs(""), client.lcs(key1, key3).get()); + assertEquals(gs("bcd"), client.lcs(key1, key2).get()); + + // non set keys are used + client.sadd(notExistingKey, new GlideString[] {gs("setmember")}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcs(notExistingKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcs_with_len_option(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonStringKey = "{key}-4" + UUID.randomUUID(); + + // keys does not exist or is empty + assertEquals(0, client.lcsLen(key1, key2).get()); + + // setting string values + client.set(key1, "abcd"); + client.set(key2, "bcde"); + client.set(key3, "wxyz"); + + // getting the lcs + assertEquals(0, client.lcsLen(key1, key3).get()); + assertEquals(3, client.lcsLen(key1, key2).get()); + + // non set keys are used + client.sadd(nonStringKey, new String[] {"setmember"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcs(nonStringKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcs_with_len_option_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString notExistingKey = gs("{key}-4" + UUID.randomUUID()); + + // keys does not exist or is empty + assertEquals(0, client.lcsLen(key1, key2).get()); + + // setting string values + client.set(key1, gs("abcd")); + client.set(key2, gs("bcde")); + client.set(key3, gs("wxyz")); + + // getting the lcs + assertEquals(0, client.lcsLen(key1, key3).get()); + assertEquals(3, client.lcsLen(key1, key2).get()); + + // non set keys are used + client.sadd(notExistingKey, new GlideString[] {gs("setmember")}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcs(notExistingKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sunion(BaseClient client) { + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonSetKey = "{key}-4" + UUID.randomUUID(); + String[] memberList1 = new String[] {"a", "b", "c"}; + String[] memberList2 = new String[] {"b", "c", "d", "e"}; + Set expectedUnion = Set.of("a", "b", "c", "d", "e"); + + assertEquals(3, client.sadd(key1, memberList1).get()); + assertEquals(4, client.sadd(key2, memberList2).get()); + assertEquals(expectedUnion, client.sunion(new String[] {key1, key2}).get()); + + // Key has an empty set + assertEquals(Set.of(), client.sunion(new String[] {key3}).get()); + + // Empty key with non-empty key returns non-empty key set + assertEquals(Set.of(memberList1), client.sunion(new String[] {key1, key3}).get()); + + // Exceptions + // Empty keys + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sunion(new String[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Non-set key + assertEquals(OK, client.set(nonSetKey, "value").get()); + assertThrows( + ExecutionException.class, () -> client.sunion(new String[] {nonSetKey, key1}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sunion_binary(BaseClient client) { + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString nonSetKey = gs("{key}-4" + UUID.randomUUID()); + GlideString[] memberList1 = new GlideString[] {gs("a"), gs("b"), gs("c")}; + GlideString[] memberList2 = new GlideString[] {gs("b"), gs("c"), gs("d"), gs("e")}; + Set expectedUnion = Set.of(gs("a"), gs("b"), gs("c"), gs("d"), gs("e")); + + assertEquals(3, client.sadd(key1, memberList1).get()); + assertEquals(4, client.sadd(key2, memberList2).get()); + assertEquals(expectedUnion, client.sunion(new GlideString[] {key1, key2}).get()); + + // Key has an empty set + assertEquals(Set.of(), client.sunion(new GlideString[] {key3}).get()); + + // Empty key with non-empty key returns non-empty key set + assertEquals(Set.of(memberList1), client.sunion(new GlideString[] {key1, key3}).get()); + + // Exceptions + // Empty keys + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sunion(new GlideString[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Non-set key + assertEquals(OK, client.set(nonSetKey, gs("value")).get()); + assertThrows( + ExecutionException.class, () -> client.sunion(new GlideString[] {nonSetKey, key1}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void test_dump_restore(BaseClient client) { + String key = UUID.randomUUID().toString(); + String newKey1 = UUID.randomUUID().toString(); + String newKey2 = UUID.randomUUID().toString(); + String nonExistingKey = UUID.randomUUID().toString(); + String value = "oranges"; + + assertEquals(OK, client.set(key, value).get()); + + // Dump existing key + byte[] result = client.dump(gs(key)).get(); + assertNotNull(result); + + // Dump non-existing key + assertNull(client.dump(gs(nonExistingKey)).get()); + + // Restore to a new key + assertEquals(OK, client.restore(gs(newKey1), 0L, result).get()); + + // Restore to an existing key - Error: "Target key name already exists" + Exception executionException = + assertThrows(ExecutionException.class, () -> client.restore(gs(newKey1), 0L, result).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Restore with checksum error - Error: "payload version or checksum are wrong" + executionException = + assertThrows( + ExecutionException.class, + () -> client.restore(gs(newKey2), 0L, value.getBytes()).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void test_dump_restore_withOptions(BaseClient client) { + String key = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String newKey = UUID.randomUUID().toString(); + String value = "oranges"; + + assertEquals(OK, client.set(key, value).get()); + + // Dump existing key + byte[] data = client.dump(gs(key)).get(); + assertNotNull(data); + + // Restore without option + String result = client.restore(gs(newKey), 0L, data).get(); + assertEquals(OK, result); + + // Restore with REPLACE option + result = client.restore(gs(newKey), 0L, data, RestoreOptions.builder().replace().build()).get(); + assertEquals(OK, result); + + // Restore with REPLACE and existing key holding different value + assertEquals(1, client.sadd(key2, new String[] {"a"}).get()); + result = client.restore(gs(key2), 0L, data, RestoreOptions.builder().replace().build()).get(); + assertEquals(OK, result); + + // Restore with REPLACE, ABSTTL, and positive TTL + result = + client + .restore(gs(newKey), 1000L, data, RestoreOptions.builder().replace().absttl().build()) + .get(); + assertEquals(OK, result); + + // Restore with REPLACE, ABSTTL, and negative TTL + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .restore( + gs(newKey), -10L, data, RestoreOptions.builder().replace().absttl().build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Restore with REPLACE and positive idletime + result = + client + .restore(gs(newKey), 0L, data, RestoreOptions.builder().replace().idletime(10L).build()) + .get(); + assertEquals(OK, result); + + // Restore with REPLACE and negative idletime + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .restore( + gs(newKey), + 0L, + data, + RestoreOptions.builder().replace().idletime(-10L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Restore with REPLACE and positive frequency + result = + client + .restore( + gs(newKey), 0L, data, RestoreOptions.builder().replace().frequency(10L).build()) + .get(); + assertEquals(OK, result); + + // Restore with REPLACE and negative frequency + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .restore( + gs(newKey), + 0L, + data, + RestoreOptions.builder().replace().frequency(-10L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sort(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + + assertArrayEquals(new String[0], client.sort(key3).get()); + assertEquals(4, client.lpush(key1, key1LpushArgs).get()); + assertArrayEquals(key1AscendingList, client.sort(key1).get()); + + // SORT_R0 + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals(new String[0], client.sortReadOnly(key3).get()); + assertArrayEquals(key1AscendingList, client.sortReadOnly(key1).get()); + } + + // SORT with STORE + assertEquals(4, client.sortStore(key1, key3).get()); + assertArrayEquals(key1AscendingList, client.lrange(key3, 0, -1).get()); + + // Exceptions + // SORT with strings require ALPHA + assertEquals(7, client.lpush(key2, key2LpushArgs).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sort(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sort_binary(BaseClient client) { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString[] key1LpushArgs = {gs("2"), gs("1"), gs("4"), gs("3")}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + GlideString[] key1AscendingList_gs = {gs("1"), gs("2"), gs("3"), gs("4")}; + GlideString[] key2LpushArgs = {gs("2"), gs("1"), gs("a"), gs("x"), gs("c"), gs("4"), gs("3")}; + + assertArrayEquals(new GlideString[0], client.sort(key3).get()); + assertEquals(4, client.lpush(key1, key1LpushArgs).get()); + assertArrayEquals(key1AscendingList_gs, client.sort(key1).get()); + + // SORT_R0 + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals(new GlideString[0], client.sortReadOnly(key3).get()); + assertArrayEquals(key1AscendingList_gs, client.sortReadOnly(key1).get()); + } + + // SORT with STORE + assertEquals(4, client.sortStore(key1, key3).get()); + assertArrayEquals(key1AscendingList, client.lrange(key3.toString(), 0, -1).get()); + + // Exceptions + // SORT with strings require ALPHA + assertEquals(7, client.lpush(key2, key2LpushArgs).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sort(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcsIdx(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonStringKey = "{key}-4" + UUID.randomUUID(); + + // keys does not exist or is empty + Map result = client.lcsIdx(key1, key2).get(); + assertDeepEquals(new Object[0], result.get("matches")); + assertEquals(0L, result.get("len")); + result = client.lcsIdx(key1, key2, 10L).get(); + assertDeepEquals(new Object[0], result.get("matches")); + assertEquals(0L, result.get("len")); + result = client.lcsIdxWithMatchLen(key1, key2).get(); + assertDeepEquals(new Object[0], result.get("matches")); + assertEquals(0L, result.get("len")); + + // setting string values + client.set(key1, "abcdefghijk"); + client.set(key2, "defjkjuighijk"); + + // LCS with only IDX + Object expectedMatchesObject = new Long[][][] {{{6L, 10L}, {8L, 12L}}, {{3L, 5L}, {0L, 2L}}}; + result = client.lcsIdx(key1, key2).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX and WITHMATCHLEN + expectedMatchesObject = + new Object[] { + new Object[] {new Long[] {6L, 10L}, new Long[] {8L, 12L}, 5L}, + new Object[] {new Long[] {3L, 5L}, new Long[] {0L, 2L}, 3L} + }; + result = client.lcsIdxWithMatchLen(key1, key2).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX and MINMATCHLEN + expectedMatchesObject = new Long[][][] {{{6L, 10L}, {8L, 12L}}}; + result = client.lcsIdx(key1, key2, 4).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX and a negative MINMATCHLEN + expectedMatchesObject = new Long[][][] {{{6L, 10L}, {8L, 12L}}, {{3L, 5L}, {0L, 2L}}}; + result = client.lcsIdx(key1, key2, -1L).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX, MINMATCHLEN, and WITHMATCHLEN + expectedMatchesObject = + new Object[] {new Object[] {new Long[] {6L, 10L}, new Long[] {8L, 12L}, 5L}}; + result = client.lcsIdxWithMatchLen(key1, key2, 4L).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // non-string keys are used + client.sadd(nonStringKey, new String[] {"setmember"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcsIdx(nonStringKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows(ExecutionException.class, () -> client.lcsIdx(nonStringKey, key1, 10L).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.lcsIdxWithMatchLen(nonStringKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.lcsIdxWithMatchLen(nonStringKey, key1, 10L).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcsIdx_binary(BaseClient client) { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7.0.0"); + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString notExistingKey = gs("{key}-4" + UUID.randomUUID()); + + // keys does not exist or is empty + Map result = client.lcsIdx(key1, key2).get(); + assertDeepEquals(new Object[0], result.get("matches")); + assertEquals(0L, result.get("len")); + result = client.lcsIdx(key1, key2, 10L).get(); + assertDeepEquals(new Object[0], result.get("matches")); + assertEquals(0L, result.get("len")); + result = client.lcsIdxWithMatchLen(key1, key2).get(); + assertDeepEquals(new Object[0], result.get("matches")); + assertEquals(0L, result.get("len")); + + // setting string values + client.set(key1, gs("abcdefghijk")); + client.set(key2, gs("defjkjuighijk")); + + // LCS with only IDX + Object expectedMatchesObject = new Long[][][] {{{6L, 10L}, {8L, 12L}}, {{3L, 5L}, {0L, 2L}}}; + result = client.lcsIdx(key1, key2).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX and WITHMATCHLEN + expectedMatchesObject = + new Object[] { + new Object[] {new Long[] {6L, 10L}, new Long[] {8L, 12L}, 5L}, + new Object[] {new Long[] {3L, 5L}, new Long[] {0L, 2L}, 3L} + }; + result = client.lcsIdxWithMatchLen(key1, key2).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX and MINMATCHLEN + expectedMatchesObject = new Long[][][] {{{6L, 10L}, {8L, 12L}}}; + result = client.lcsIdx(key1, key2, 4).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX and a negative MINMATCHLEN + expectedMatchesObject = new Long[][][] {{{6L, 10L}, {8L, 12L}}, {{3L, 5L}, {0L, 2L}}}; + result = client.lcsIdx(key1, key2, -1L).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // LCS with IDX, MINMATCHLEN, and WITHMATCHLEN + expectedMatchesObject = + new Object[] {new Object[] {new Long[] {6L, 10L}, new Long[] {8L, 12L}, 5L}}; + result = client.lcsIdxWithMatchLen(key1, key2, 4L).get(); + assertDeepEquals(expectedMatchesObject, result.get("matches")); + assertEquals(8L, result.get("len")); + + // non-string keys are used + client.sadd(notExistingKey, new GlideString[] {gs("setmember")}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcsIdx(notExistingKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.lcsIdx(notExistingKey, key1, 10L).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, () -> client.lcsIdxWithMatchLen(notExistingKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> client.lcsIdxWithMatchLen(notExistingKey, key1, 10L).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geosearch(BaseClient client) { + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String[] members = {"Catania", "Palermo", "edge2", "edge1"}; + Set members_set = Set.of(members); + GeospatialData[] members_coordinates = { + new GeospatialData(15.087269, 37.502669), + new GeospatialData(13.361389, 38.115556), + new GeospatialData(17.241510, 38.788135), + new GeospatialData(12.758489, 38.788135) + }; + Object[] expectedResult = { + new Object[] { + "Catania", + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + "Palermo", + new Object[] { + 190.4424, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + }, + new Object[] { + "edge2", + new Object[] { + 279.7403, 3481342659049484L, new Object[] {17.241510450839996, 38.78813451624225} + } + }, + new Object[] { + "edge1", + new Object[] { + 279.7405, 3479273021651468L, new Object[] {12.75848776102066, 38.78813451624225} + } + }, + }; + + // geoadd + assertEquals( + 4, + client + .geoadd( + key1, + Map.of( + members[0], + members_coordinates[0], + members[1], + members_coordinates[1], + members[2], + members_coordinates[2], + members[3], + members_coordinates[3])) + .get()); + + // Search by box, unit: km, from a geospatial data point + assertTrue( + members_set.containsAll( + Set.of( + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS)) + .get()))); + + assertArrayEquals( + members, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + assertDeepEquals( + expectedResult, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withcoord().withdist().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + assertDeepEquals( + new Object[] {new Object[] {"Catania", new Object[] {56.4413, 3479447370796909L}}}, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withdist().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC, 1)) + .get()); + + // test search by box, unit: meters, from member, with distance + long meters = 400 * 1000; + assertDeepEquals( + new Object[] { + new Object[] {"edge2", new Object[] {236529.1799}}, + new Object[] {"Palermo", new Object[] {166274.1516}}, + new Object[] {"Catania", new Object[] {0.0}}, + }, + client + .geosearch( + key1, + new MemberOrigin("Catania"), + new GeoSearchShape(meters, meters, GeoUnit.METERS), + GeoSearchOptions.builder().withdist().build(), + new GeoSearchResultOptions(SortOrder.DESC)) + .get()); + + // test search by box, unit: feet, from member, with limited count 2, with hash + double feet = 400 * 3280.8399; + assertDeepEquals( + new Object[] { + new Object[] {"Palermo", new Object[] {3479099956230698L}}, + new Object[] {"edge1", new Object[] {3479273021651468L}}, + }, + client + .geosearch( + key1, + new MemberOrigin("Palermo"), + new GeoSearchShape(feet, feet, GeoUnit.FEET), + GeoSearchOptions.builder().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)) + .get()); + + // test search by box, unit: miles, from geospatial position, with limited ANY count to 1 + ArrayUtils.contains( + members, + client.geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(250, 250, GeoUnit.MILES), + new GeoSearchResultOptions(1, true)) + .get()[0]); + + // test search by radius, units: feet, from member + double feet_radius = 200 * 3280.8399; + assertArrayEquals( + new String[] {"Catania", "Palermo"}, + client + .geosearch( + key1, + new MemberOrigin("Catania"), + new GeoSearchShape(feet_radius, GeoUnit.FEET), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + // Test search by radius, unit: meters, from member + double meters_radius = 200 * 1000; + assertArrayEquals( + new String[] {"Palermo", "Catania"}, + client + .geosearch( + key1, + new MemberOrigin("Catania"), + new GeoSearchShape(meters_radius, GeoUnit.METERS), + new GeoSearchResultOptions(SortOrder.DESC)) + .get()); + assertDeepEquals( + new Object[] { + new Object[] {"Palermo", new Object[] {3479099956230698L}}, + new Object[] {"Catania", new Object[] {3479447370796909L}}, + }, + client + .geosearch( + key1, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(meters_radius, GeoUnit.METERS), + GeoSearchOptions.builder().withhash().build()) + .get()); + + // Test search by radius, unit: miles, from geospatial data + assertArrayEquals( + new String[] {"edge1", "edge2", "Palermo", "Catania"}, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(175, GeoUnit.MILES), + new GeoSearchResultOptions(SortOrder.DESC)) + .get()); + + // Test search by radius, unit: kilometers, from a geospatial data, with limited count to 2 + assertDeepEquals( + new Object[] { + new Object[] { + "Catania", + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + "Palermo", + new Object[] { + 190.4424, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + } + }, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withdist().withhash().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)) + .get()); + + // Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 + assertTrue( + ArrayUtils.contains( + members, + ((Object[]) + client.geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withdist().withhash().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 1, true)) + .get()[0]) + [0])); + + // no members within the area + assertArrayEquals( + new String[] {}, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(50, 50, GeoUnit.METERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + // no members within the area + assertArrayEquals( + new String[] {}, + client + .geosearch( + key1, + new GeoSearchOrigin.CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(5, GeoUnit.METERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + // member does not exist + ExecutionException requestException1 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearch( + key1, + new MemberOrigin("non-existing-member"), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException1.getCause()); + + // key exists but holds a non-ZSET value + assertEquals(OK, client.set(key2, "nonZSETvalue").get()); + ExecutionException requestException2 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearch( + key2, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geosearch_binary(BaseClient client) { + // setup + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString[] members = {gs("Catania"), gs("Palermo"), gs("edge2"), gs("edge1")}; + Set members_set = Set.of(members); + GeospatialData[] members_coordinates = { + new GeospatialData(15.087269, 37.502669), + new GeospatialData(13.361389, 38.115556), + new GeospatialData(17.241510, 38.788135), + new GeospatialData(12.758489, 38.788135) + }; + Object[] expectedResult = { + new Object[] { + gs("Catania"), + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + gs("Palermo"), + new Object[] { + 190.4424, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + }, + new Object[] { + gs("edge2"), + new Object[] { + 279.7403, 3481342659049484L, new Object[] {17.241510450839996, 38.78813451624225} + } + }, + new Object[] { + gs("edge1"), + new Object[] { + 279.7405, 3479273021651468L, new Object[] {12.75848776102066, 38.78813451624225} + } + }, + }; + + // geoadd + assertEquals( + 4, + client + .geoadd( + key1, + Map.of( + members[0], + members_coordinates[0], + members[1], + members_coordinates[1], + members[2], + members_coordinates[2], + members[3], + members_coordinates[3])) + .get()); + + // Search by box, unit: km, from a geospatial data point + assertTrue( + members_set.containsAll( + Set.of( + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS)) + .get()))); + + assertArrayEquals( + members, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + assertDeepEquals( + expectedResult, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withcoord().withdist().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + assertDeepEquals( + new Object[] {new Object[] {gs("Catania"), new Object[] {56.4413, 3479447370796909L}}}, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withdist().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC, 1)) + .get()); + + // test search by box, unit: meters, from member, with distance + long meters = 400 * 1000; + assertDeepEquals( + new Object[] { + new Object[] {gs("edge2"), new Object[] {236529.1799}}, + new Object[] {gs("Palermo"), new Object[] {166274.1516}}, + new Object[] {gs("Catania"), new Object[] {0.0}}, + }, + client + .geosearch( + key1, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(meters, meters, GeoUnit.METERS), + GeoSearchOptions.builder().withdist().build(), + new GeoSearchResultOptions(SortOrder.DESC)) + .get()); + + // test search by box, unit: feet, from member, with limited count 2, with hash + double feet = 400 * 3280.8399; + assertDeepEquals( + new Object[] { + new Object[] {gs("Palermo"), new Object[] {3479099956230698L}}, + new Object[] {gs("edge1"), new Object[] {3479273021651468L}}, + }, + client + .geosearch( + key1, + new MemberOriginBinary(gs("Palermo")), + new GeoSearchShape(feet, feet, GeoUnit.FEET), + GeoSearchOptions.builder().withhash().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)) + .get()); + + // test search by box, unit: miles, from geospatial position, with limited ANY count to 1 + ArrayUtils.contains( + members, + client.geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(250, 250, GeoUnit.MILES), + new GeoSearchResultOptions(1, true)) + .get()[0]); + + // test search by radius, units: feet, from member + double feet_radius = 200 * 3280.8399; + assertArrayEquals( + new GlideString[] {gs("Catania"), gs("Palermo")}, + client + .geosearch( + key1, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(feet_radius, GeoUnit.FEET), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + // Test search by radius, unit: meters, from member + double meters_radius = 200 * 1000; + assertArrayEquals( + new GlideString[] {gs("Palermo"), gs("Catania")}, + client + .geosearch( + key1, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(meters_radius, GeoUnit.METERS), + new GeoSearchResultOptions(SortOrder.DESC)) + .get()); + + assertDeepEquals( + new Object[] { + new Object[] {gs("Palermo"), new Object[] {3479099956230698L}}, + new Object[] {gs("Catania"), new Object[] {3479447370796909L}}, + }, + client + .geosearch( + key1, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(meters_radius, GeoUnit.METERS), + GeoSearchOptions.builder().withhash().build()) + .get()); + // Test search by radius, unit: miles, from geospatial data + assertArrayEquals( + new GlideString[] {gs("edge1"), gs("edge2"), gs("Palermo"), gs("Catania")}, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(175, GeoUnit.MILES), + new GeoSearchResultOptions(SortOrder.DESC)) + .get()); + + // Test search by radius, unit: kilometers, from a geospatial data, with limited count to 2 + assertDeepEquals( + new Object[] { + new Object[] { + gs("Catania"), + new Object[] { + 56.4413, 3479447370796909L, new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + gs("Palermo"), + new Object[] { + 190.4424, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + } + }, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withdist().withhash().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)) + .get()); + + // Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 + assertTrue( + ArrayUtils.contains( + members, + ((Object[]) + client.geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withdist().withhash().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 1, true)) + .get()[0]) + [0])); + + // no members within the area + assertArrayEquals( + new GlideString[] {}, + client + .geosearch( + key1, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(50, 50, GeoUnit.METERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + // no members within the area + assertArrayEquals( + new GlideString[] {}, + client + .geosearch( + key1, + new GeoSearchOrigin.CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(5, GeoUnit.METERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .get()); + + // member does not exist + ExecutionException requestException1 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearch( + key1, + new MemberOriginBinary(gs("non-existing-member")), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException1.getCause()); + + // key exists but holds a non-ZSET value + assertEquals(OK, client.set(key2, gs("nonZSETvalue")).get()); + ExecutionException requestException2 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearch( + key2, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geosearchstore(BaseClient client) { + // setup + String sourceKey = "{key}-1" + UUID.randomUUID(); + String destinationKey = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] members = {"Catania", "Palermo", "edge2", "edge1"}; + GeospatialData[] members_coordinates = { + new GeospatialData(15.087269, 37.502669), + new GeospatialData(13.361389, 38.115556), + new GeospatialData(17.241510, 38.788135), + new GeospatialData(12.758489, 38.788135) + }; + Map expectedMap = + Map.of( + "Catania", 3479447370796909.0, + "Palermo", 3479099956230698.0, + "edge2", 3481342659049484.0, + "edge1", 3479273021651468.0); + Map expectedMap2 = + Map.of( + "Catania", 56.4412578701582, + "Palermo", 190.44242984775784, + "edge2", 279.7403417843143, + "edge1", 279.7404521356343); + Map expectedMap3 = + Map.of( + "Catania", 3479447370796909.0, + "Palermo", 3479099956230698.0); + Map expectedMap4 = + Map.of( + "Catania", 56.4412578701582, + "Palermo", 190.44242984775784); + + // geoadd + assertEquals( + 4, + client + .geoadd( + sourceKey, + Map.of( + members[0], + members_coordinates[0], + members[1], + members_coordinates[1], + members[2], + members_coordinates[2], + members[3], + members_coordinates[3])) + .get()); + + // Test storing results of a box search, unit: kilometers, from a geospatial data position + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS)) + .get(), + 4L); + + // Verify the stored results + Map zrange_map = + client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap, zrange_map); + + // Test storing results of a box search, unit: kilometes, from a geospatial data position, with + // distance + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build()) + .get(), + 4L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap2, zrange_map); + + // Test storing results of a box search, unit: kilometes, from a geospatial data, with count + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(1)) + .get(), + 1L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals(Map.of("Catania", 3479447370796909.0), zrange_map); + + // Test storing results of a box search, unit: meters, from a member, with distance + double metersValue = 400 * 1000; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("Catania"), + new GeoSearchShape(metersValue, metersValue, GeoUnit.METERS), + GeoSearchStoreOptions.builder().storedist().build()) + .get(), + 3L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals( + Map.of( + "Catania", 0.0, + "Palermo", 166274.15156960033, + "edge2", 236529.17986494553), + zrange_map); + + // Test search by box, unit: feet, from a member, with limited ANY count to 2, with hash + double feetValue = 400 * 3280.8399; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("Palermo"), + new GeoSearchShape(feetValue, feetValue, GeoUnit.FEET), + new GeoSearchResultOptions(2)) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + for (String memberKey : zrange_map.keySet()) { + assertTrue(expectedMap.containsKey(memberKey)); + } + + // Test storing results of a radius search, unit: feet, from a member + double feetValue2 = 200 * 3280.8399; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("Catania"), + new GeoSearchShape(feetValue2, GeoUnit.FEET)) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap3, zrange_map); + + // Test search by radius, units: meters, from a member + double metersValue2 = 200 * 1000; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("Catania"), + new GeoSearchShape(metersValue2, GeoUnit.METERS), + GeoSearchStoreOptions.builder().storedist().build()) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals( + Map.of( + "Catania", 0.0, + "Palermo", 166274.15156960033), + zrange_map); + + // Test search by radius, unit: miles, from a geospatial data + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(175, GeoUnit.MILES)) + .get(), + 4L); + + // Test storing results of a radius search, unit: kilometers, from a geospatial data, with + // limited count to 2 + double kmValue = 200.0; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(kmValue, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build(), + new GeoSearchResultOptions(2)) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap4, zrange_map); + + // Test storing results of a radius search, unit: kilometers, from a geospatial data, with + // limited ANY count to 1 + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(kmValue, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(1, true)) + .get(), + 1L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey, new RangeByIndex(0, -1)).get(); + for (String memberKey : zrange_map.keySet()) { + assertTrue(expectedMap.containsKey(memberKey)); + } + + // Test no members within the area + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(50, 50, GeoUnit.METERS)) + .get(), + 0L); + + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(1, GeoUnit.METERS)) + .get(), + 0L); + + // No members in the area (apart from the member we search from itself) + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("Catania"), + new GeoSearchShape(10, 10, GeoUnit.METERS)) + .get(), + 1L); + + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("Catania"), + new GeoSearchShape(10, GeoUnit.METERS)) + .get(), + 1L); + + // member does not exist + ExecutionException requestException1 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOrigin("non-existing-member"), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException1.getCause()); + + // key exists but holds a non-ZSET value + assertEquals(OK, client.set(key3, "nonZSETvalue").get()); + ExecutionException requestException2 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearchstore( + key3, + key3, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void geosearchstore_binary(BaseClient client) { + // setup + GlideString sourceKey = gs("{key}-1" + UUID.randomUUID()); + GlideString destinationKey = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString[] members = {gs("Catania"), gs("Palermo"), gs("edge2"), gs("edge1")}; + GeospatialData[] members_coordinates = { + new GeospatialData(15.087269, 37.502669), + new GeospatialData(13.361389, 38.115556), + new GeospatialData(17.241510, 38.788135), + new GeospatialData(12.758489, 38.788135) + }; + Map expectedMap = + Map.of( + "Catania", 3479447370796909.0, + "Palermo", 3479099956230698.0, + "edge2", 3481342659049484.0, + "edge1", 3479273021651468.0); + Map expectedMap2 = + Map.of( + "Catania", 56.4412578701582, + "Palermo", 190.44242984775784, + "edge2", 279.7403417843143, + "edge1", 279.7404521356343); + Map expectedMap3 = + Map.of( + "Catania", 3479447370796909.0, + "Palermo", 3479099956230698.0); + Map expectedMap4 = + Map.of( + "Catania", 56.4412578701582, + "Palermo", 190.44242984775784); + + // geoadd + assertEquals( + 4, + client + .geoadd( + sourceKey, + Map.of( + members[0], + members_coordinates[0], + members[1], + members_coordinates[1], + members[2], + members_coordinates[2], + members[3], + members_coordinates[3])) + .get()); + + // Test storing results of a box search, unit: kilometers, from a geospatial data position + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS)) + .get(), + 4L); + + // Verify the stored results + Map zrange_map = + client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap, zrange_map); + + // Test storing results of a box search, unit: kilometes, from a geospatial data position, with + // distance + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build()) + .get(), + 4L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap2, zrange_map); + + // Test storing results of a box search, unit: kilometes, from a geospatial data, with count + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(1)) + .get(), + 1L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals(Map.of("Catania", 3479447370796909.0), zrange_map); + + // Test storing results of a box search, unit: meters, from a member, with distance + double metersValue = 400 * 1000; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(metersValue, metersValue, GeoUnit.METERS), + GeoSearchStoreOptions.builder().storedist().build()) + .get(), + 3L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals( + Map.of( + "Catania", 0.0, + "Palermo", 166274.15156960033, + "edge2", 236529.17986494553), + zrange_map); + + // Test search by box, unit: feet, from a member, with limited ANY count to 2, with hash + double feetValue = 400 * 3280.8399; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("Palermo")), + new GeoSearchShape(feetValue, feetValue, GeoUnit.FEET), + new GeoSearchResultOptions(2)) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + for (String memberKey : zrange_map.keySet()) { + assertTrue(expectedMap.containsKey(memberKey)); + } + + // Test storing results of a radius search, unit: feet, from a member + double feetValue2 = 200 * 3280.8399; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(feetValue2, GeoUnit.FEET)) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap3, zrange_map); + + // Test search by radius, units: meters, from a member + double metersValue2 = 200 * 1000; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(metersValue2, GeoUnit.METERS), + GeoSearchStoreOptions.builder().storedist().build()) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals( + Map.of( + "Catania", 0.0, + "Palermo", 166274.15156960033), + zrange_map); + + // Test search by radius, unit: miles, from a geospatial data + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(175, GeoUnit.MILES)) + .get(), + 4L); + + // Test storing results of a radius search, unit: kilometers, from a geospatial data, with + // limited count to 2 + double kmValue = 200.0; + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(kmValue, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build(), + new GeoSearchResultOptions(2)) + .get(), + 2L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + assertDeepEquals(expectedMap4, zrange_map); + + // Test storing results of a radius search, unit: kilometers, from a geospatial data, with + // limited ANY count to 1 + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(kmValue, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(1, true)) + .get(), + 1L); + + // Verify stored results + zrange_map = client.zrangeWithScores(destinationKey.toString(), new RangeByIndex(0, -1)).get(); + for (String memberKey : zrange_map.keySet()) { + assertTrue(expectedMap.containsKey(memberKey)); + } + + // Test no members within the area + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(50, 50, GeoUnit.METERS)) + .get(), + 0L); + + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(1, GeoUnit.METERS)) + .get(), + 0L); + + // No members in the area (apart from the member we search from itself) + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(10, 10, GeoUnit.METERS)) + .get(), + 1L); + + assertEquals( + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("Catania")), + new GeoSearchShape(10, GeoUnit.METERS)) + .get(), + 1L); + + // member does not exist + ExecutionException requestException1 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearchstore( + destinationKey, + sourceKey, + new MemberOriginBinary(gs("non-existing-member")), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException1.getCause()); + + // key exists but holds a non-ZSET value + assertEquals(OK, client.set(key3, gs("nonZSETvalue")).get()); + ExecutionException requestException2 = + assertThrows( + ExecutionException.class, + () -> + client + .geosearchstore( + key3, + key3, + new CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(100, GeoUnit.METERS)) + .get()); + assertInstanceOf(RequestException.class, requestException2.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sscan(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String initialCursor = "0"; + long defaultCount = 10; + String[] numberMembers = new String[50000]; // Use large dataset to force an iterative cursor. + for (int i = 0; i < numberMembers.length; i++) { + numberMembers[i] = String.valueOf(i); + } + Set numberMembersSet = Set.of(numberMembers); + String[] charMembers = new String[] {"a", "b", "c", "d", "e"}; + Set charMemberSet = Set.of(charMembers); + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Empty set + Object[] result = client.sscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Negative cursor + result = client.sscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Result contains the whole set + assertEquals(charMembers.length, client.sadd(key1, charMembers).get()); + result = client.sscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertEquals(charMembers.length, ((Object[]) result[resultCollectionIndex]).length); + final Set resultMembers = + Arrays.stream((Object[]) result[resultCollectionIndex]).collect(Collectors.toSet()); + assertTrue( + resultMembers.containsAll(charMemberSet), + String.format("resultMembers: {%s}, charMemberSet: {%s}", resultMembers, charMemberSet)); + + result = + client.sscan(key1, initialCursor, SScanOptions.builder().matchPattern("a").build()).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {"a"}, result[resultCollectionIndex]); + + // Result contains a subset of the key + assertEquals(numberMembers.length, client.sadd(key1, numberMembers).get()); + String resultCursor = "0"; + final Set secondResultValues = new HashSet<>(); + boolean isFirstLoop = true; + do { + result = client.sscan(key1, resultCursor).get(); + resultCursor = result[resultCursorIndex].toString(); + secondResultValues.addAll( + Arrays.stream((Object[]) result[resultCollectionIndex]).collect(Collectors.toSet())); + + if (isFirstLoop) { + assertNotEquals("0", resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals("0")) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.sscan(key1, resultCursor).get(); + String newResultCursor = secondResult[resultCursorIndex].toString(); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); + secondResultValues.addAll( + Arrays.stream((Object[]) secondResult[resultCollectionIndex]) + .collect(Collectors.toSet())); + } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + assertTrue( + secondResultValues.containsAll(numberMembersSet), + String.format( + "secondResultValues: {%s}, numberMembersSet: {%s}", + secondResultValues, numberMembersSet)); + + assertTrue( + secondResultValues.containsAll(numberMembersSet), + String.format( + "secondResultValues: {%s}, numberMembersSet: {%s}", + secondResultValues, numberMembersSet)); + + // Test match pattern + result = + client.sscan(key1, initialCursor, SScanOptions.builder().matchPattern("*").build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); + + // Test count + result = client.sscan(key1, initialCursor, SScanOptions.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); + + // Test count with match returns a non-empty list + result = + client + .sscan( + key1, initialCursor, SScanOptions.builder().matchPattern("1*").count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 0); + + // Exceptions + // Non-set key + assertEquals(OK, client.set(key2, "test").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .sscan( + key2, + initialCursor, + SScanOptions.builder().matchPattern("test").count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> client.sscan(key1, "-1", SScanOptions.builder().count(-1L).build()).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sscan_binary(BaseClient client) { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString initialCursor = gs("0"); + long defaultCount = 10; + GlideString[] numberMembers = + new GlideString[50000]; // Use large dataset to force an iterative cursor. + for (int i = 0; i < numberMembers.length; i++) { + numberMembers[i] = gs(String.valueOf(i)); + } + Set numberMembersSet = Set.of(numberMembers); + GlideString[] charMembers = new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e")}; + Set charMemberSet = Set.of(charMembers); + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Empty set + Object[] result = client.sscan(key1, initialCursor).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + + // Negative cursor + result = client.sscan(key1, gs("-1")).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + + // Result contains the whole set + assertEquals(charMembers.length, client.sadd(key1, charMembers).get()); + result = client.sscan(key1, initialCursor).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertEquals(charMembers.length, ((Object[]) result[resultCollectionIndex]).length); + final Set resultMembers = + Arrays.stream((Object[]) result[resultCollectionIndex]).collect(Collectors.toSet()); + assertTrue( + resultMembers.containsAll(charMemberSet), + String.format("resultMembers: {%s}, charMemberSet: {%s}", resultMembers, charMemberSet)); + + result = + client + .sscan(key1, initialCursor, SScanOptionsBinary.builder().matchPattern(gs("a")).build()) + .get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {gs("a")}, result[resultCollectionIndex]); + + // Result contains a subset of the key + assertEquals(numberMembers.length, client.sadd(key1, numberMembers).get()); + GlideString resultCursor = gs("0"); + final Set secondResultValues = new HashSet<>(); + boolean isFirstLoop = true; + do { + result = client.sscan(key1, resultCursor).get(); + resultCursor = gs(result[resultCursorIndex].toString()); + secondResultValues.addAll( + Arrays.stream((Object[]) result[resultCollectionIndex]).collect(Collectors.toSet())); + + if (isFirstLoop) { + assertNotEquals(gs("0"), resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals(gs("0"))) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.sscan(key1, resultCursor).get(); + GlideString newResultCursor = gs(secondResult[resultCursorIndex].toString()); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); + secondResultValues.addAll( + Arrays.stream((Object[]) secondResult[resultCollectionIndex]) + .collect(Collectors.toSet())); + } while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. + + assertTrue( + secondResultValues.containsAll(numberMembersSet), + String.format( + "secondResultValues: {%s}, numberMembersSet: {%s}", + secondResultValues, numberMembersSet)); + + assertTrue( + secondResultValues.containsAll(numberMembersSet), + String.format( + "secondResultValues: {%s}, numberMembersSet: {%s}", + secondResultValues, numberMembersSet)); + + // Test match pattern + result = + client + .sscan(key1, initialCursor, SScanOptionsBinary.builder().matchPattern(gs("*")).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); + + // Test count + result = + client.sscan(key1, initialCursor, SScanOptionsBinary.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); + + // Test count with match returns a non-empty list + result = + client + .sscan( + key1, + initialCursor, + SScanOptionsBinary.builder().matchPattern(gs("1*")).count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 0); + + // Exceptions + // Non-set key + assertEquals(OK, client.set(key2, gs("test")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .sscan( + key2, + initialCursor, + SScanOptionsBinary.builder().matchPattern(gs("test")).count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .sscan(key1, gs("-1"), SScanOptionsBinary.builder().count(-1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zscan(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String initialCursor = "0"; + long defaultCount = 20; + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Setup test data - use a large number of entries to force an iterative cursor. + Map numberMap = new HashMap<>(); + for (Double i = 0.0; i < 50000; i++) { + numberMap.put(String.valueOf(i), i); + } + String[] charMembers = new String[] {"a", "b", "c", "d", "e"}; + Map charMap = new HashMap<>(); + for (double i = 0.0; i < 5; i++) { + charMap.put(charMembers[(int) i], i); + } + + // Empty set + Object[] result = client.zscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Negative cursor + result = client.zscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Result contains the whole set + assertEquals(charMembers.length, client.zadd(key1, charMap).get()); + result = client.zscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertEquals( + charMap.size() * 2, + ((Object[]) result[resultCollectionIndex]) + .length); // Length includes the score which is twice the map size + final Object[] resultArray = (Object[]) result[resultCollectionIndex]; + + final Set resultKeys = new HashSet<>(); + final Set resultValues = new HashSet<>(); + for (int i = 0; i < resultArray.length; i += 2) { + resultKeys.add(resultArray[i]); + resultValues.add(resultArray[i + 1]); + } + assertTrue( + resultKeys.containsAll(charMap.keySet()), + String.format("resultKeys: {%s} charMap.keySet(): {%s}", resultKeys, charMap.keySet())); + + // The score comes back as an integer converted to a String when the fraction is zero. + final Set expectedScoresAsStrings = + charMap.values().stream() + .map(v -> String.valueOf(v.intValue())) + .collect(Collectors.toSet()); + + assertTrue( + resultValues.containsAll(expectedScoresAsStrings), + String.format( + "resultValues: {%s} expectedScoresAsStrings: {%s}", + resultValues, expectedScoresAsStrings)); + + result = + client.zscan(key1, initialCursor, ZScanOptions.builder().matchPattern("a").build()).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {"a", "0"}, result[resultCollectionIndex]); + + // Result contains a subset of the key + assertEquals(numberMap.size(), client.zadd(key1, numberMap).get()); + String resultCursor = "0"; + final Set secondResultAllKeys = new HashSet<>(); + final Set secondResultAllValues = new HashSet<>(); + boolean isFirstLoop = true; + do { + result = client.zscan(key1, resultCursor).get(); + resultCursor = result[resultCursorIndex].toString(); + Object[] resultEntry = (Object[]) result[resultCollectionIndex]; + for (int i = 0; i < resultEntry.length; i += 2) { + secondResultAllKeys.add(resultEntry[i]); + secondResultAllValues.add(resultEntry[i + 1]); + } + + if (isFirstLoop) { + assertNotEquals("0", resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals("0")) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.zscan(key1, resultCursor).get(); + String newResultCursor = secondResult[resultCursorIndex].toString(); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + Object[] secondResultEntry = (Object[]) secondResult[resultCollectionIndex]; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); + + for (int i = 0; i < secondResultEntry.length; i += 2) { + secondResultAllKeys.add(secondResultEntry[i]); + secondResultAllValues.add(secondResultEntry[i + 1]); + } + } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + assertTrue( + secondResultAllKeys.containsAll(numberMap.keySet()), + String.format( + "secondResultAllKeys: {%s} numberMap.keySet: {%s}", + secondResultAllKeys, numberMap.keySet())); + + final Set numberMapValuesAsStrings = + numberMap.values().stream() + .map(d -> String.valueOf(d.intValue())) + .collect(Collectors.toSet()); + + assertTrue( + secondResultAllValues.containsAll(numberMapValuesAsStrings), + String.format( + "secondResultAllValues: {%s} numberMapValuesAsStrings: {%s}", + secondResultAllValues, numberMapValuesAsStrings)); + + // Test match pattern + result = + client.zscan(key1, initialCursor, ZScanOptions.builder().matchPattern("*").build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); + + // Test count + result = client.zscan(key1, initialCursor, ZScanOptions.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); + + // Test count with match returns a non-empty list + result = + client + .zscan( + key1, initialCursor, ZScanOptions.builder().matchPattern("1*").count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 0); + + // Exceptions + // Non-set key + assertEquals(OK, client.set(key2, "test").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zscan( + key2, + initialCursor, + ZScanOptions.builder().matchPattern("test").count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> client.zscan(key1, "-1", ZScanOptions.builder().count(-1L).build()).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zscan_binary(BaseClient client) { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString initialCursor = gs("0"); + long defaultCount = 20; + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Setup test data - use a large number of entries to force an iterative cursor. + Map numberMap = new HashMap<>(); + for (Double i = 0.0; i < 50000; i++) { + numberMap.put(gs(String.valueOf(i)), i); + } + Map numberMap_strings = new HashMap<>(); + for (Double i = 0.0; i < 50000; i++) { + numberMap_strings.put(String.valueOf(i), i); + } + + GlideString[] charMembers = new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e")}; + Map charMap = new HashMap<>(); + for (double i = 0.0; i < 5; i++) { + charMap.put(charMembers[(int) i], i); + } + Map charMap_strings = new HashMap<>(); + for (double i = 0.0; i < 5; i++) { + charMap_strings.put(charMembers[(int) i].toString(), i); + } + + // Empty set + Object[] result = client.zscan(key1, initialCursor).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + + // Negative cursor + result = client.zscan(key1, gs("-1")).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); + + // Result contains the whole set + assertEquals(charMembers.length, client.zadd(key1.toString(), charMap_strings).get()); + result = client.zscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertEquals( + charMap.size() * 2, + ((Object[]) result[resultCollectionIndex]) + .length); // Length includes the score which is twice the map size + final Object[] resultArray = (Object[]) result[resultCollectionIndex]; + + final Set resultKeys = new HashSet<>(); + final Set resultValues = new HashSet<>(); + for (int i = 0; i < resultArray.length; i += 2) { + resultKeys.add(resultArray[i]); + resultValues.add(resultArray[i + 1]); + } + assertTrue( + resultKeys.containsAll(charMap.keySet()), + String.format("resultKeys: {%s} charMap.keySet(): {%s}", resultKeys, charMap.keySet())); + + // The score comes back as an integer converted to a String when the fraction is zero. + final Set expectedScoresAsGlideStrings = + charMap.values().stream() + .map(v -> gs(String.valueOf(v.intValue()))) + .collect(Collectors.toSet()); + + assertTrue( + resultValues.containsAll(expectedScoresAsGlideStrings), + String.format( + "resultValues: {%s} expectedScoresAsStrings: {%s}", + resultValues, expectedScoresAsGlideStrings)); + + result = + client + .zscan(key1, initialCursor, ZScanOptionsBinary.builder().matchPattern(gs("a")).build()) + .get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new GlideString[] {gs("a"), gs("0")}, result[resultCollectionIndex]); + + // Result contains a subset of the key + assertEquals(numberMap.size(), client.zadd(key1.toString(), numberMap_strings).get()); + GlideString resultCursor = gs("0"); + final Set secondResultAllKeys = new HashSet<>(); + final Set secondResultAllValues = new HashSet<>(); + boolean isFirstLoop = true; + do { + result = client.zscan(key1, resultCursor).get(); + resultCursor = gs(result[resultCursorIndex].toString()); + Object[] resultEntry = (Object[]) result[resultCollectionIndex]; + for (int i = 0; i < resultEntry.length; i += 2) { + secondResultAllKeys.add(resultEntry[i]); + secondResultAllValues.add(resultEntry[i + 1]); + } + + if (isFirstLoop) { + assertNotEquals(gs("0"), resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals("0")) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.zscan(key1, resultCursor).get(); + GlideString newResultCursor = gs(secondResult[resultCursorIndex].toString()); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + Object[] secondResultEntry = (Object[]) secondResult[resultCollectionIndex]; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); + + for (int i = 0; i < secondResultEntry.length; i += 2) { + secondResultAllKeys.add(secondResultEntry[i]); + secondResultAllValues.add(secondResultEntry[i + 1]); + } + } while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. + + assertTrue( + secondResultAllKeys.containsAll(numberMap.keySet()), + String.format( + "secondResultAllKeys: {%s} numberMap.keySet: {%s}", + secondResultAllKeys, numberMap.keySet())); + + final Set numberMapValuesAsGlideStrings = + numberMap.values().stream() + .map(d -> gs(String.valueOf(d.intValue()))) + .collect(Collectors.toSet()); + + assertTrue( + secondResultAllValues.containsAll(numberMapValuesAsGlideStrings), + String.format( + "secondResultAllValues: {%s} numberMapValuesAsStrings: {%s}", + secondResultAllValues, numberMapValuesAsGlideStrings)); + + // Test match pattern + result = + client + .zscan(key1, initialCursor, ZScanOptionsBinary.builder().matchPattern(gs("*")).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); - assertEquals(1, client.lpush(key1, new String[] {"0"}).get()); - assertEquals(4, client.lpushx(key1, new String[] {"1", "2", "3"}).get()); - assertArrayEquals(new String[] {"3", "2", "1", "0"}, client.lrange(key1, 0, -1).get()); + // Test count + result = + client.zscan(key1, initialCursor, ZScanOptionsBinary.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); - assertEquals(0, client.lpushx(key2, new String[] {"1"}).get()); - assertArrayEquals(new String[0], client.lrange(key2, 0, -1).get()); + // Test count with match returns a non-empty list + result = + client + .zscan( + key1, + initialCursor, + ZScanOptionsBinary.builder().matchPattern(gs("1*")).count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 0); - // Key exists, but it is not a list - assertEquals(OK, client.set(key3, "bar").get()); + // Exceptions + // Non-set key + assertEquals(OK, client.set(key2, gs("test")).get()); ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.lpushx(key3, new String[] {"_"}).get()); - // empty element list + assertThrows(ExecutionException.class, () -> client.zscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = - assertThrows(ExecutionException.class, () -> client.lpushx(key2, new String[0]).get()); - assertTrue(executionException.getCause() instanceof RequestException); + assertThrows( + ExecutionException.class, + () -> + client + .zscan( + key2, + initialCursor, + ZScanOptionsBinary.builder().matchPattern(gs("test")).count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .zscan(key1, gs("-1"), ZScanOptionsBinary.builder().count(-1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zrange_by_index(BaseClient client) { - String key = UUID.randomUUID().toString(); - Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); - assertEquals(3, client.zadd(key, membersScores).get()); + public void hscan(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String initialCursor = "0"; + long defaultCount = 20; + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Setup test data + Map numberMap = new HashMap<>(); + // This is an unusually large dataset because the server can ignore the COUNT option + // if the dataset is small enough that it is more efficient to transfer its entire contents + // at once. + for (int i = 0; i < 50000; i++) { + numberMap.put(String.valueOf(i), "num" + i); + } + String[] charMembers = new String[] {"a", "b", "c", "d", "e"}; + Map charMap = new HashMap<>(); + for (int i = 0; i < 5; i++) { + charMap.put(charMembers[i], String.valueOf(i)); + } - RangeByIndex query = new RangeByIndex(0, 1); - assertArrayEquals(new String[] {"one", "two"}, client.zrange(key, query).get()); + // Empty set + Object[] result = client.hscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); - query = new RangeByIndex(0, -1); + // Negative cursor + result = client.hscan(key1, "-1").get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {}, result[resultCollectionIndex]); + + // Result contains the whole set + assertEquals(charMembers.length, client.hset(key1, charMap).get()); + result = client.hscan(key1, initialCursor).get(); + assertEquals(initialCursor, result[resultCursorIndex]); assertEquals( - Map.of("one", 1.0, "two", 2.0, "three", 3.0), client.zrangeWithScores(key, query).get()); + charMap.size() * 2, + ((Object[]) result[resultCollectionIndex]) + .length); // Length includes the score which is twice the map size + final Object[] resultArray = (Object[]) result[resultCollectionIndex]; + + final Set resultKeys = new HashSet<>(); + final Set resultValues = new HashSet<>(); + for (int i = 0; i < resultArray.length; i += 2) { + resultKeys.add(resultArray[i]); + resultValues.add(resultArray[i + 1]); + } + assertTrue( + resultKeys.containsAll(charMap.keySet()), + String.format("resultKeys: {%s} charMap.keySet(): {%s}", resultKeys, charMap.keySet())); - query = new RangeByIndex(0, 1); - assertArrayEquals(new String[] {"three", "two"}, client.zrange(key, query, true).get()); + assertTrue( + resultValues.containsAll(charMap.values()), + String.format("resultValues: {%s} charMap.values(): {%s}", resultValues, charMap.values())); + + result = + client.hscan(key1, initialCursor, HScanOptions.builder().matchPattern("a").build()).get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new String[] {"a", "0"}, result[resultCollectionIndex]); + + // Result contains a subset of the key + final HashMap combinedMap = new HashMap<>(numberMap); + combinedMap.putAll(charMap); + assertEquals(numberMap.size(), client.hset(key1, combinedMap).get()); + String resultCursor = "0"; + final Set secondResultAllKeys = new HashSet<>(); + final Set secondResultAllValues = new HashSet<>(); + boolean isFirstLoop = true; + do { + result = client.hscan(key1, resultCursor).get(); + resultCursor = result[resultCursorIndex].toString(); + Object[] resultEntry = (Object[]) result[resultCollectionIndex]; + for (int i = 0; i < resultEntry.length; i += 2) { + secondResultAllKeys.add(resultEntry[i]); + secondResultAllValues.add(resultEntry[i + 1]); + } + + if (isFirstLoop) { + assertNotEquals("0", resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals("0")) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.hscan(key1, resultCursor).get(); + String newResultCursor = secondResult[resultCursorIndex].toString(); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + Object[] secondResultEntry = (Object[]) secondResult[resultCollectionIndex]; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); - query = new RangeByIndex(3, 1); - assertArrayEquals(new String[] {}, client.zrange(key, query, true).get()); - assertTrue(client.zrangeWithScores(key, query).get().isEmpty()); + for (int i = 0; i < secondResultEntry.length; i += 2) { + secondResultAllKeys.add(secondResultEntry[i]); + secondResultAllValues.add(secondResultEntry[i + 1]); + } + } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + assertTrue( + secondResultAllKeys.containsAll(numberMap.keySet()), + String.format( + "secondResultAllKeys: {%s} numberMap.keySet: {%s}", + secondResultAllKeys, numberMap.keySet())); + + assertTrue( + secondResultAllValues.containsAll(numberMap.values()), + String.format( + "secondResultAllValues: {%s} numberMap.values(): {%s}", + secondResultAllValues, numberMap.values())); + + // Test match pattern + result = + client.hscan(key1, initialCursor, HScanOptions.builder().matchPattern("*").build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); + + // Test count + result = client.hscan(key1, initialCursor, HScanOptions.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); + + // Test count with match returns a non-empty list + result = + client + .hscan( + key1, initialCursor, HScanOptions.builder().matchPattern("1*").count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 0); + + // Exceptions + // Non-hash key + assertEquals(OK, client.set(key2, "test").get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .hscan( + key2, + initialCursor, + HScanOptions.builder().matchPattern("test").count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> client.hscan(key1, "-1", HScanOptions.builder().count(-1L).build()).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zrange_by_score(BaseClient client) { - String key = UUID.randomUUID().toString(); - Map membersScores = Map.of("one", 1.0, "two", 2.0, "three", 3.0); - assertEquals(3, client.zadd(key, membersScores).get()); + public void hscan_binary(BaseClient client) { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString initialCursor = gs("0"); + long defaultCount = 20; + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Setup test data + Map numberMap = new HashMap<>(); + // This is an unusually large dataset because the server can ignore the COUNT option + // if the dataset is small enough that it is more efficient to transfer its entire contents + // at once. + for (int i = 0; i < 50000; i++) { + numberMap.put(gs(String.valueOf(i)), gs("num" + i)); + } + GlideString[] charMembers = new GlideString[] {gs("a"), gs("b"), gs("c"), gs("d"), gs("e")}; + Map charMap = new HashMap<>(); + for (int i = 0; i < 5; i++) { + charMap.put(charMembers[i], gs(String.valueOf(i))); + } - RangeByScore query = new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false)); - assertArrayEquals(new String[] {"one", "two"}, client.zrange(key, query).get()); + // Empty set + Object[] result = client.hscan(key1, initialCursor).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); - query = new RangeByScore(NEGATIVE_INFINITY, POSITIVE_INFINITY); - assertEquals( - Map.of("one", 1.0, "two", 2.0, "three", 3.0), client.zrangeWithScores(key, query).get()); + // Negative cursor + result = client.hscan(key1, gs("-1")).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertDeepEquals(new GlideString[] {}, result[resultCollectionIndex]); - query = new RangeByScore(new ScoreBoundary(3, false), NEGATIVE_INFINITY); - assertArrayEquals(new String[] {"two", "one"}, client.zrange(key, query, true).get()); + // Result contains the whole set + assertEquals(charMembers.length, client.hset(key1, charMap).get()); + result = client.hscan(key1, initialCursor).get(); + assertEquals(initialCursor, gs(result[resultCursorIndex].toString())); + assertEquals( + charMap.size() * 2, + ((Object[]) result[resultCollectionIndex]) + .length); // Length includes the score which is twice the map size + final Object[] resultArray = (Object[]) result[resultCollectionIndex]; + + final Set resultKeys = new HashSet<>(); + final Set resultValues = new HashSet<>(); + for (int i = 0; i < resultArray.length; i += 2) { + resultKeys.add(resultArray[i]); + resultValues.add(resultArray[i + 1]); + } + assertTrue( + resultKeys.containsAll(charMap.keySet()), + String.format("resultKeys: {%s} charMap.keySet(): {%s}", resultKeys, charMap.keySet())); - query = new RangeByScore(NEGATIVE_INFINITY, POSITIVE_INFINITY, new Limit(1, 2)); - assertArrayEquals(new String[] {"two", "three"}, client.zrange(key, query).get()); + assertTrue( + resultValues.containsAll(charMap.values()), + String.format("resultValues: {%s} charMap.values(): {%s}", resultValues, charMap.values())); - query = new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false)); - assertArrayEquals( - new String[] {}, + result = client - .zrange(key, query, true) - .get()); // stop is greater than start with reverse set to True + .hscan(key1, initialCursor, HScanOptionsBinary.builder().matchPattern(gs("a")).build()) + .get(); + assertEquals(initialCursor, result[resultCursorIndex]); + assertDeepEquals(new GlideString[] {gs("a"), gs("0")}, result[resultCollectionIndex]); + + // Result contains a subset of the key + final HashMap combinedMap = new HashMap<>(numberMap); + combinedMap.putAll(charMap); + assertEquals(numberMap.size(), client.hset(key1, combinedMap).get()); + GlideString resultCursor = gs("0"); + final Set secondResultAllKeys = new HashSet<>(); + final Set secondResultAllValues = new HashSet<>(); + boolean isFirstLoop = true; + do { + result = client.hscan(key1, resultCursor).get(); + resultCursor = gs(result[resultCursorIndex].toString()); + Object[] resultEntry = (Object[]) result[resultCollectionIndex]; + for (int i = 0; i < resultEntry.length; i += 2) { + secondResultAllKeys.add(resultEntry[i]); + secondResultAllValues.add(resultEntry[i + 1]); + } + + if (isFirstLoop) { + assertNotEquals(gs("0"), resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals(gs("0"))) { + break; + } + + // Scan with result cursor has a different set + Object[] secondResult = client.hscan(key1, resultCursor).get(); + GlideString newResultCursor = gs(secondResult[resultCursorIndex].toString()); + assertNotEquals(resultCursor, newResultCursor); + resultCursor = newResultCursor; + Object[] secondResultEntry = (Object[]) secondResult[resultCollectionIndex]; + assertFalse( + Arrays.deepEquals( + ArrayUtils.toArray(result[resultCollectionIndex]), + ArrayUtils.toArray(secondResult[resultCollectionIndex]))); - query = new RangeByScore(POSITIVE_INFINITY, new ScoreBoundary(3, false)); - assertArrayEquals( - new String[] {}, client.zrange(key, query, true).get()); // start is greater than stop + for (int i = 0; i < secondResultEntry.length; i += 2) { + secondResultAllKeys.add(secondResultEntry[i]); + secondResultAllValues.add(secondResultEntry[i + 1]); + } + } while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. - query = new RangeByScore(POSITIVE_INFINITY, new ScoreBoundary(3, false)); - assertTrue(client.zrangeWithScores(key, query).get().isEmpty()); // start is greater than stop + assertTrue( + secondResultAllKeys.containsAll(numberMap.keySet()), + String.format( + "secondResultAllKeys: {%s} numberMap.keySet: {%s}", + secondResultAllKeys, numberMap.keySet())); - query = new RangeByScore(NEGATIVE_INFINITY, new ScoreBoundary(3, false)); assertTrue( - client - .zrangeWithScores(key, query, true) - .get() - .isEmpty()); // stop is greater than start with reverse set to True - } + secondResultAllValues.containsAll(numberMap.values()), + String.format( + "secondResultAllValues: {%s} numberMap.values(): {%s}", + secondResultAllValues, numberMap.values())); - @SneakyThrows - @ParameterizedTest(autoCloseArguments = false) - @MethodSource("getClients") - public void zrange_by_lex(BaseClient client) { - String key = UUID.randomUUID().toString(); - Map membersScores = Map.of("a", 1.0, "b", 2.0, "c", 3.0); - assertEquals(3, client.zadd(key, membersScores).get()); + // Test match pattern + result = + client + .hscan(key1, initialCursor, HScanOptionsBinary.builder().matchPattern(gs("*")).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= defaultCount); - RangeByLex query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); - assertArrayEquals(new String[] {"a", "b"}, client.zrange(key, query).get()); + // Test count + result = + client.hscan(key1, initialCursor, HScanOptionsBinary.builder().count(20L).build()).get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 20); - query = - new RangeByLex( - InfLexBound.NEGATIVE_INFINITY, InfLexBound.POSITIVE_INFINITY, new Limit(1, 2)); - assertArrayEquals(new String[] {"b", "c"}, client.zrange(key, query).get()); + // Test count with match returns a non-empty list + result = + client + .hscan( + key1, + initialCursor, + HScanOptionsBinary.builder().matchPattern(gs("1*")).count(20L).build()) + .get(); + assertTrue(Long.parseLong(result[resultCursorIndex].toString()) >= 0); + assertTrue(ArrayUtils.getLength(result[resultCollectionIndex]) >= 0); - query = new RangeByLex(new LexBoundary("c", false), InfLexBound.NEGATIVE_INFINITY); - assertArrayEquals(new String[] {"b", "a"}, client.zrange(key, query, true).get()); + // Exceptions + // Non-hash key + assertEquals(OK, client.set(key2, gs("test")).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.hscan(key2, initialCursor).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); - query = new RangeByLex(InfLexBound.NEGATIVE_INFINITY, new LexBoundary("c", false)); - assertArrayEquals( - new String[] {}, - client - .zrange(key, query, true) - .get()); // stop is greater than start with reverse set to True + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .hscan( + key2, + initialCursor, + HScanOptionsBinary.builder().matchPattern(gs("test")).count(1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); - query = new RangeByLex(InfLexBound.POSITIVE_INFINITY, new LexBoundary("c", false)); - assertArrayEquals( - new String[] {}, client.zrange(key, query).get()); // start is greater than stop + // Negative count + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .hscan(key1, gs("-1"), HScanOptionsBinary.builder().count(-1L).build()) + .get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void zrange_with_different_types_of_keys(BaseClient client) { + public void waitTest(BaseClient client) { + // setup String key = UUID.randomUUID().toString(); - RangeByIndex query = new RangeByIndex(0, 1); - - assertArrayEquals(new String[] {}, client.zrange("non_existing_key", query).get()); - - assertTrue( - client - .zrangeWithScores("non_existing_key", query) - .get() - .isEmpty()); // start is greater than stop + long numreplicas = 1L; + long timeout = 1000L; - // Key exists, but it is not a set + // assert that wait returns 0 under standalone and 1 under cluster mode. assertEquals(OK, client.set(key, "value").get()); - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.zrange(key, query).get()); - assertTrue(executionException.getCause() instanceof RequestException); + assertTrue(client.wait(numreplicas, timeout).get() >= (client instanceof GlideClient ? 0 : 1)); - executionException = - assertThrows(ExecutionException.class, () -> client.zrangeWithScores(key, query).get()); - assertTrue(executionException.getCause() instanceof RequestException); + // command should fail on a negative timeout value + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.wait(1L, -1L).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void pfadd(BaseClient client) { + public void wait_timeout_check(BaseClient client) { String key = UUID.randomUUID().toString(); - assertEquals(1, client.pfadd(key, new String[0]).get()); - assertEquals(1, client.pfadd(key, new String[] {"one", "two"}).get()); - assertEquals(0, client.pfadd(key, new String[] {"two"}).get()); - assertEquals(0, client.pfadd(key, new String[0]).get()); - - // Key exists, but it is not a HyperLogLog - assertEquals(OK, client.set("foo", "bar").get()); - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.pfadd("foo", new String[0]).get()); - assertTrue(executionException.getCause() instanceof RequestException); + // create new client with default request timeout (250 millis) + try (var testClient = + client instanceof GlideClient + ? GlideClient.createClient(commonClientConfig().build()).get() + : GlideClusterClient.createClient(commonClusterClientConfig().build()).get()) { + + // ensure that commands do not time out, even if timeout > request timeout + assertEquals(OK, testClient.set(key, "value").get()); + assertEquals((client instanceof GlideClient ? 0 : 1), testClient.wait(1L, 1000L).get()); + + // with 0 timeout (no timeout) wait should block indefinitely, + // but we wrap the test with timeout to avoid test failing or being stuck forever + assertEquals(OK, testClient.set(key, "value2").get()); + assertThrows( + TimeoutException.class, // <- future timeout, not command timeout + () -> testClient.wait(100L, 0L).get(1000, TimeUnit.MILLISECONDS)); + } } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void pfcount(BaseClient client) { - String key1 = "{test}-hll1-" + UUID.randomUUID(); - String key2 = "{test}-hll2-" + UUID.randomUUID(); - String key3 = "{test}-hll3-" + UUID.randomUUID(); - assertEquals(1, client.pfadd(key1, new String[] {"a", "b", "c"}).get()); - assertEquals(1, client.pfadd(key2, new String[] {"b", "c", "d"}).get()); - assertEquals(3, client.pfcount(new String[] {key1}).get()); - assertEquals(3, client.pfcount(new String[] {key2}).get()); - assertEquals(4, client.pfcount(new String[] {key1, key2}).get()); - assertEquals(4, client.pfcount(new String[] {key1, key2, key3}).get()); - // empty HyperLogLog data set - assertEquals(1, client.pfadd(key3, new String[0]).get()); - assertEquals(0, client.pfcount(new String[] {key3}).get()); + public void xinfoStream(BaseClient client) { + String key = UUID.randomUUID().toString(); + String groupName = "group" + UUID.randomUUID(); + String consumer = "consumer" + UUID.randomUUID(); + String streamId0_0 = "0-0"; + String streamId1_0 = "1-0"; + String streamId1_1 = "1-1"; + + // Setup: add stream entry, create consumer group and consumer, read from stream with consumer + final LinkedHashMap dataToAdd = + new LinkedHashMap<>() { + { + put("a", "b"); + put("c", "d"); + } + }; + assertEquals( + streamId1_0, + client.xadd(key, dataToAdd, StreamAddOptions.builder().id(streamId1_0).build()).get()); + assertEquals(OK, client.xgroupCreate(key, groupName, streamId0_0).get()); + client.xreadgroup(Map.of(key, ">"), groupName, consumer).get(); + + Map result = client.xinfoStream(key).get(); + assertEquals(1L, result.get("length")); + Object[] expectedFirstEntry = new Object[] {streamId1_0, new String[] {"a", "b", "c", "d"}}; + assertDeepEquals(expectedFirstEntry, result.get("first-entry")); + + // Only one entry exists, so first and last entry should be the same + assertDeepEquals(expectedFirstEntry, result.get("last-entry")); + + // Call XINFO STREAM with a byte string arg + Map result2 = client.xinfoStream(gs(key)).get(); + assertEquals(1L, result2.get(gs("length"))); + Object[] gsFirstEntry = (Object[]) result2.get(gs("first-entry")); + Object[] expectedFirstEntryGs = + new Object[] {gs(streamId1_0), new Object[] {gs("a"), gs("b"), gs("c"), gs("d")}}; + assertArrayEquals(expectedFirstEntryGs, gsFirstEntry); + + // Add one more entry + assertEquals( + streamId1_1, + client + .xadd(key, Map.of("foo", "bar"), StreamAddOptions.builder().id(streamId1_1).build()) + .get()); - // Key exists, but it is not a HyperLogLog - assertEquals(OK, client.set("foo", "bar").get()); - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> client.pfcount(new String[] {"foo"}).get()); - assertTrue(executionException.getCause() instanceof RequestException); + result = client.xinfoStreamFull(key, 1).get(); + assertEquals(2L, result.get("length")); + Object[] entries = (Object[]) result.get("entries"); + // Only the first entry will be returned since we passed count=1 + assertEquals(1, entries.length); + assertDeepEquals(new Object[] {expectedFirstEntry}, entries); + + Object[] groups = (Object[]) result.get("groups"); + assertEquals(1, groups.length); + Map groupInfo = (Map) groups[0]; + assertEquals(groupName, groupInfo.get("name")); + Object[] pending = (Object[]) groupInfo.get("pending"); + assertEquals(1, pending.length); + assertEquals(true, Arrays.toString((Object[]) pending[0]).contains(streamId1_0)); + + Object[] consumers = (Object[]) groupInfo.get("consumers"); + assertEquals(1, consumers.length); + Map consumersInfo = (Map) consumers[0]; + assertEquals(consumer, consumersInfo.get("name")); + Object[] consumersPending = (Object[]) consumersInfo.get("pending"); + assertEquals(1, consumersPending.length); + assertTrue(Arrays.toString((Object[]) consumersPending[0]).contains(streamId1_0)); + + // Call XINFO STREAM FULL with byte arg + Map resultFull2 = client.xinfoStreamFull(gs(key)).get(); + // 2 entries should be returned, since we didn't pass the COUNT arg this time + assertEquals(2, ((Object[]) resultFull2.get(gs("entries"))).length); } @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") - public void pfmerge(BaseClient client) { - String key1 = "{test}-hll1-" + UUID.randomUUID(); - String key2 = "{test}-hll2-" + UUID.randomUUID(); - String key3 = "{test}-hll3-" + UUID.randomUUID(); - assertEquals(1, client.pfadd(key1, new String[] {"a", "b", "c"}).get()); - assertEquals(1, client.pfadd(key2, new String[] {"b", "c", "d"}).get()); - // new HyperLogLog data set - assertEquals(OK, client.pfmerge(key3, new String[] {key1, key2}).get()); - assertEquals( - client.pfcount(new String[] {key1, key2}).get(), client.pfcount(new String[] {key3}).get()); - // existing HyperLogLog data set - assertEquals(OK, client.pfmerge(key1, new String[] {key2}).get()); + public void xinfoStream_edge_cases_and_failures(BaseClient client) { + String key = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String nonExistentKey = UUID.randomUUID().toString(); + String streamId1_0 = "1-0"; + + // Setup: create empty stream assertEquals( - client.pfcount(new String[] {key1, key2}).get(), client.pfcount(new String[] {key1}).get()); + streamId1_0, + client + .xadd(key, Map.of("field", "value"), StreamAddOptions.builder().id(streamId1_0).build()) + .get()); + assertEquals(1, client.xdel(key, new String[] {streamId1_0}).get()); - // Key exists, but it is not a HyperLogLog - assertEquals(OK, client.set("foo", "bar").get()); + // XINFO STREAM called against empty stream + Map result = client.xinfoStream(key).get(); + assertEquals(0L, result.get("length")); + assertDeepEquals(null, result.get("first-entry")); + assertDeepEquals(null, result.get("last-entry")); + + // XINFO STREAM FULL called against empty stream. Negative count values are ignored. + Map resultFull = client.xinfoStreamFull(key, -3).get(); + assertEquals(0L, resultFull.get("length")); + assertDeepEquals(new Object[] {}, resultFull.get("entries")); + assertDeepEquals(new Object[] {}, resultFull.get("groups")); + + // Calling XINFO STREAM with a non-existing key raises an error ExecutionException executionException = - assertThrows( - ExecutionException.class, () -> client.pfmerge("foo", new String[] {key1}).get()); - assertTrue(executionException.getCause() instanceof RequestException); + assertThrows(ExecutionException.class, () -> client.xinfoStream(nonExistentKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.xinfoStreamFull(nonExistentKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); executionException = assertThrows( - ExecutionException.class, () -> client.pfmerge(key1, new String[] {"foo"}).get()); - assertTrue(executionException.getCause() instanceof RequestException); + ExecutionException.class, () -> client.xinfoStreamFull(nonExistentKey, 1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // Key exists, but it is not a stream + assertEquals(OK, client.set(stringKey, "foo").get()); + executionException = + assertThrows(ExecutionException.class, () -> client.xinfoStream(stringKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.xinfoStreamFull(stringKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); } } diff --git a/java/integTest/src/test/java/glide/TestConfiguration.java b/java/integTest/src/test/java/glide/TestConfiguration.java index 31e6489523..7bf788746e 100644 --- a/java/integTest/src/test/java/glide/TestConfiguration.java +++ b/java/integTest/src/test/java/glide/TestConfiguration.java @@ -1,14 +1,14 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; import com.vdurmont.semver4j.Semver; import java.util.Arrays; public final class TestConfiguration { - // All redis servers are hosted on localhost - public static final int[] STANDALONE_PORTS = getPortsFromProperty("test.redis.standalone.ports"); - public static final int[] CLUSTER_PORTS = getPortsFromProperty("test.redis.cluster.ports"); - public static final Semver REDIS_VERSION = new Semver(System.getProperty("test.redis.version")); + // All servers are hosted on localhost + public static final int[] STANDALONE_PORTS = getPortsFromProperty("test.server.standalone.ports"); + public static final int[] CLUSTER_PORTS = getPortsFromProperty("test.server.cluster.ports"); + public static final Semver SERVER_VERSION = new Semver(System.getProperty("test.server.version")); private static int[] getPortsFromProperty(String propName) { return Arrays.stream(System.getProperty(propName).split(",")) diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index a50d4542f5..55d5a69d55 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -1,17 +1,29 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.api.models.GlideString.gs; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import glide.api.BaseClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; -import glide.api.models.configuration.RedisClusterClientConfiguration; import java.security.SecureRandom; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import lombok.experimental.UtilityClass; @@ -65,15 +77,298 @@ public static Map parseInfoResponseToMap(String serverInfo) { HashMap::new)); } - public static RedisClientConfiguration.RedisClientConfigurationBuilder + public static GlideClientConfiguration.GlideClientConfigurationBuilder commonClientConfig() { - return RedisClientConfiguration.builder() + return GlideClientConfiguration.builder() .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build()); } - public static RedisClusterClientConfiguration.RedisClusterClientConfigurationBuilder + public static GlideClusterClientConfiguration.GlideClusterClientConfigurationBuilder commonClusterClientConfig() { - return RedisClusterClientConfiguration.builder() + return GlideClusterClientConfiguration.builder() .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build()); } + + /** + * Deep traverse and compare two objects, including comparing content of all nested collections + * recursively. Floating point numbers comparison performed with 1e-6 delta. + * + * @apiNote Map and Set comparison ignores element order.
+ * List and Array comparison is order-sensitive. + */ + public static void assertDeepEquals(Object expected, Object actual) { + if (expected == null || actual == null) { + assertEquals(expected, actual); + } else if (expected.getClass().isArray()) { + var expectedArray = (Object[]) expected; + var actualArray = (Object[]) actual; + assertEquals(expectedArray.length, actualArray.length); + for (int i = 0; i < expectedArray.length; i++) { + assertDeepEquals(expectedArray[i], actualArray[i]); + } + } else if (expected instanceof List) { + var expectedList = (List) expected; + var actualList = (List) actual; + assertEquals(expectedList.size(), actualList.size()); + for (int i = 0; i < expectedList.size(); i++) { + assertDeepEquals(expectedList.get(i), actualList.get(i)); + } + } else if (expected instanceof Set) { + var expectedSet = (Set) expected; + var actualSet = (Set) actual; + assertEquals(expectedSet.size(), actualSet.size()); + assertTrue(expectedSet.containsAll(actualSet) && actualSet.containsAll(expectedSet)); + } else if (expected instanceof Map) { + var expectedMap = (Map) expected; + var actualMap = (Map) actual; + assertEquals(expectedMap.size(), actualMap.size()); + for (var key : expectedMap.keySet()) { + assertDeepEquals(expectedMap.get(key), actualMap.get(key)); + } + } else if (expected instanceof Double || actual instanceof Double) { + assertEquals((Double) expected, (Double) actual, 1e-6); + } else { + assertEquals(expected, actual); + } + } + + /** + * Validate whether `FUNCTION LIST` response contains required info. + * + * @param response The response from valkey. + * @param libName Expected library name. + * @param functionDescriptions Expected function descriptions. Key - function name, value - + * description. + * @param functionFlags Expected function flags. Key - function name, value - flags set. + * @param libCode Expected library to check if given. + */ + @SuppressWarnings("unchecked") + public static void checkFunctionListResponse( + Map[] response, + String libName, + Map functionDescriptions, + Map> functionFlags, + Optional libCode) { + assertTrue(response.length > 0); + boolean hasLib = false; + for (var lib : response) { + hasLib = lib.containsValue(libName); + if (hasLib) { + var functions = (Object[]) lib.get("functions"); + assertEquals(functionDescriptions.size(), functions.length); + for (var functionInfo : functions) { + var function = (Map) functionInfo; + var functionName = (String) function.get("name"); + assertEquals(functionDescriptions.get(functionName), function.get("description")); + assertEquals(functionFlags.get(functionName), function.get("flags")); + } + if (libCode.isPresent()) { + assertEquals(libCode.get(), lib.get("library_code")); + } + break; + } + } + assertTrue(hasLib); + } + + private void assertSetsEqual(Set expected, Set actual) { + // Convert both sets to lists. It is needed due to issue that rust return the flags as string + List expectedList = + expected.stream().sorted().map(GlideString::of).collect(Collectors.toList()); + List actualList = + actual.stream().sorted().map(GlideString::of).collect(Collectors.toList()); + + assertEquals(expectedList, actualList); + } + + /** + * Validate whether `FUNCTION LIST` response contains required info. + * + * @param response The response from valkey. + * @param libName Expected library name. + * @param functionDescriptions Expected function descriptions. Key - function name, value - + * description. + * @param functionFlags Expected function flags. Key - function name, value - flags set. + * @param libCode Expected library to check if given. + */ + @SuppressWarnings("unchecked") + public static void checkFunctionListResponseBinary( + Map[] response, + GlideString libName, + Map functionDescriptions, + Map> functionFlags, + Optional libCode) { + assertTrue(response.length > 0); + boolean hasLib = false; + for (var lib : response) { + hasLib = lib.containsValue(libName); + if (hasLib) { + var functions = (Object[]) lib.get(gs("functions")); + assertEquals(functionDescriptions.size(), functions.length); + for (var functionInfo : functions) { + var function = (Map) functionInfo; + var functionName = (GlideString) function.get(gs("name")); + assertEquals(functionDescriptions.get(functionName), function.get(gs("description"))); + assertSetsEqual( + functionFlags.get(functionName), (Set) function.get(gs("flags"))); + } + if (libCode.isPresent()) { + assertEquals(libCode.get(), lib.get(gs("library_code"))); + } + break; + } + } + assertTrue(hasLib); + } + + /** + * Validate whether `FUNCTION STATS` response contains required info. + * + * @param response The response from server. + * @param runningFunction Command line of running function expected. Empty, if nothing expected. + * @param libCount Expected libraries count. + * @param functionCount Expected functions count. + */ + public static void checkFunctionStatsResponse( + Map> response, + String[] runningFunction, + long libCount, + long functionCount) { + Map runningScriptInfo = response.get("running_script"); + if (runningScriptInfo == null && runningFunction.length != 0) { + fail("No running function info"); + } + if (runningScriptInfo != null && runningFunction.length == 0) { + String[] command = (String[]) runningScriptInfo.get("command"); + fail("Unexpected running function info: " + String.join(" ", command)); + } + + if (runningScriptInfo != null) { + String[] command = (String[]) runningScriptInfo.get("command"); + assertArrayEquals(runningFunction, command); + // command line format is: + // fcall|fcall_ro * * + assertEquals(runningFunction[1], runningScriptInfo.get("name")); + } + var expected = + Map.of("LUA", Map.of("libraries_count", libCount, "functions_count", functionCount)); + assertEquals(expected, response.get("engines")); + } + + /** + * Validate whether `FUNCTION STATS` response contains required info. + * + * @param response The response from server. + * @param runningFunction Command line of running function expected. Empty, if nothing expected. + * @param libCount Expected libraries count. + * @param functionCount Expected functions count. + */ + public static void checkFunctionStatsBinaryResponse( + Map> response, + GlideString[] runningFunction, + long libCount, + long functionCount) { + Map runningScriptInfo = response.get(gs("running_script")); + if (runningScriptInfo == null && runningFunction.length != 0) { + fail("No running function info"); + } + if (runningScriptInfo != null && runningFunction.length == 0) { + GlideString[] command = (GlideString[]) runningScriptInfo.get(gs("command")); + fail("Unexpected running function info: " + String.join(" ", Arrays.toString(command))); + } + + if (runningScriptInfo != null) { + GlideString[] command = (GlideString[]) runningScriptInfo.get(gs("command")); + assertArrayEquals(runningFunction, command); + // command line format is: + // fcall|fcall_ro * * + assertEquals(runningFunction[1], runningScriptInfo.get(gs("name"))); + } + var expected = + Map.of( + gs("LUA"), + Map.of(gs("libraries_count"), libCount, gs("functions_count"), functionCount)); + assertEquals(expected, response.get(gs("engines"))); + } + + /** Generate a String of LUA library code. */ + public static String generateLuaLibCode( + String libName, Map functions, boolean readonly) { + StringBuilder code = new StringBuilder("#!lua name=" + libName + "\n"); + for (var function : functions.entrySet()) { + code.append("redis.register_function{ function_name = '") + .append(function.getKey()) + .append("', callback = function(keys, args) ") + .append(function.getValue()) + .append(" end"); + if (readonly) { + code.append(", flags = { 'no-writes' }"); + } + code.append(" }\n"); + } + return code.toString(); + } + + /** Generate a Glidestring of LUA library code. */ + public static GlideString generateLuaLibCodeBinary( + GlideString libName, Map functions, boolean readonly) { + + Map transformedMap = + functions.entrySet().stream() + .collect( + Collectors.toMap( + entry -> entry.getKey().toString(), entry -> entry.getValue().toString())); + + return gs(generateLuaLibCode(libName.toString(), transformedMap, readonly)); + } + + /** + * Create a lua lib with a RO function which runs an endless loop up to timeout sec.
+ * Execution takes at least 5 sec regardless of the timeout configured.
+ */ + public static String createLuaLibWithLongRunningFunction( + String libName, String funcName, int timeout, boolean readOnly) { + String code = + "#!lua name=$libName\n" + + "local function $libName_$funcName(keys, args)\n" + + " local started = tonumber(redis.pcall('time')[1])\n" + // fun fact - redis does no writes if 'no-writes' flag is set + + " redis.pcall('set', keys[1], 42)\n" + + " while (true) do\n" + + " local now = tonumber(redis.pcall('time')[1])\n" + + " if now > started + $timeout then\n" + + " return 'Timed out $timeout sec'\n" + + " end\n" + + " end\n" + + " return 'OK'\n" + + "end\n" + + "redis.register_function{\n" + + "function_name='$funcName',\n" + + "callback=$libName_$funcName,\n" + + (readOnly ? "flags={ 'no-writes' }\n" : "") + + "}"; + return code.replace("$timeout", Integer.toString(timeout)) + .replace("$funcName", funcName) + .replace("$libName", libName); + } + + public static void waitForNotBusy(BaseClient client) { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + boolean isBusy = true; + do { + try { + if (client instanceof GlideClusterClient) { + ((GlideClusterClient) client).functionKill().get(); + } else if (client instanceof GlideClient) { + ((GlideClient) client).functionKill().get(); + } + } catch (Exception busy) { + // should throw `notbusy` error, because the function should be killed before + if (busy.getMessage().toLowerCase().contains("notbusy")) { + isBusy = false; + } + } + } while (isBusy); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index ee039abb12..110e71e29b 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -1,271 +1,1275 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.generateLuaLibCode; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.LInsertOptions.InsertPosition.AFTER; +import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.ScoreFilter.MIN; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import glide.api.models.BaseTransaction; +import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.GetExOptions; +import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; import glide.api.models.commands.RangeOptions.RangeByIndex; import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.SetOptions; -import glide.api.models.commands.StreamAddOptions; +import glide.api.models.commands.SortOrder; +import glide.api.models.commands.WeightAggregateOptions.Aggregate; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; +import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.geospatial.GeoSearchOptions; +import glide.api.models.commands.geospatial.GeoSearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.geospatial.GeospatialData; +import glide.api.models.commands.scan.HScanOptions; +import glide.api.models.commands.scan.SScanOptions; +import glide.api.models.commands.scan.ZScanOptions; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.commands.stream.StreamClaimOptions; +import glide.api.models.commands.stream.StreamGroupOptions; +import glide.api.models.commands.stream.StreamRange; +import glide.api.models.commands.stream.StreamRange.IdBound; +import glide.api.models.commands.stream.StreamReadGroupOptions; +import glide.api.models.commands.stream.StreamReadOptions; +import glide.api.models.commands.stream.StreamTrimOptions.MinId; +import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; public class TransactionTestUtilities { - private static final String key1 = "{key}" + UUID.randomUUID(); - private static final String key2 = "{key}" + UUID.randomUUID(); - private static final String key3 = "{key}" + UUID.randomUUID(); - private static final String key4 = "{key}" + UUID.randomUUID(); - private static final String key5 = "{key}" + UUID.randomUUID(); - private static final String key6 = "{key}" + UUID.randomUUID(); - private static final String listKey3 = "{key}:listKey3-" + UUID.randomUUID(); - private static final String key7 = "{key}" + UUID.randomUUID(); - private static final String setKey2 = "{key}" + UUID.randomUUID(); - private static final String setKey3 = "{key}" + UUID.randomUUID(); - private static final String key8 = "{key}" + UUID.randomUUID(); - private static final String zSetKey2 = "{key}:zsetKey2-" + UUID.randomUUID(); - private static final String key9 = "{key}" + UUID.randomUUID(); - private static final String hllKey1 = "{key}:hllKey1-" + UUID.randomUUID(); - private static final String hllKey2 = "{key}:hllKey2-" + UUID.randomUUID(); - private static final String hllKey3 = "{key}:hllKey3-" + UUID.randomUUID(); - private static final String value1 = UUID.randomUUID().toString(); - private static final String value2 = UUID.randomUUID().toString(); - private static final String value3 = UUID.randomUUID().toString(); - private static final String field1 = UUID.randomUUID().toString(); - private static final String field2 = UUID.randomUUID().toString(); - private static final String field3 = UUID.randomUUID().toString(); - - public static BaseTransaction transactionTest(BaseTransaction baseTransaction) { - - baseTransaction.set(key1, value1); - baseTransaction.get(key1); - baseTransaction.type(key1); - - baseTransaction.set(key2, value2, SetOptions.builder().returnOldValue(true).build()); - baseTransaction.strlen(key2); - baseTransaction.customCommand(new String[] {"MGET", key1, key2}); - - baseTransaction.exists(new String[] {key1}); - baseTransaction.persist(key1); - - baseTransaction.del(new String[] {key1}); - baseTransaction.get(key1); - - baseTransaction.unlink(new String[] {key2}); - baseTransaction.get(key2); - - baseTransaction.mset(Map.of(key1, value2, key2, value1)); - baseTransaction.mget(new String[] {key1, key2}); - - baseTransaction.incr(key3); - baseTransaction.incrBy(key3, 2); - - baseTransaction.decr(key3); - baseTransaction.decrBy(key3, 2); - - baseTransaction.incrByFloat(key3, 0.5); - - baseTransaction.unlink(new String[] {key3}); - baseTransaction.setrange(key3, 0, "GLIDE"); - baseTransaction.getrange(key3, 0, 5); - - baseTransaction.hset(key4, Map.of(field1, value1, field2, value2)); - baseTransaction.hget(key4, field1); - baseTransaction.hlen(key4); - baseTransaction.hexists(key4, field2); - baseTransaction.hsetnx(key4, field1, value1); - baseTransaction.hmget(key4, new String[] {field1, "non_existing_field", field2}); - baseTransaction.hgetall(key4); - baseTransaction.hdel(key4, new String[] {field1}); - baseTransaction.hvals(key4); - - baseTransaction.hincrBy(key4, field3, 5); - baseTransaction.hincrByFloat(key4, field3, 5.5); - - baseTransaction.lpush(key5, new String[] {value1, value1, value2, value3, value3}); - baseTransaction.llen(key5); - baseTransaction.lindex(key5, 0); - baseTransaction.lrem(key5, 1, value1); - baseTransaction.ltrim(key5, 1, -1); - baseTransaction.lrange(key5, 0, -2); - baseTransaction.lpop(key5); - baseTransaction.lpopCount(key5, 2); - - baseTransaction.rpush(key6, new String[] {value1, value2, value2}); - baseTransaction.rpop(key6); - baseTransaction.rpopCount(key6, 2); - - baseTransaction.sadd(key7, new String[] {"baz", "foo"}); - baseTransaction.srem(key7, new String[] {"foo"}); - baseTransaction.scard(key7); - baseTransaction.sismember(key7, "baz"); - baseTransaction.smembers(key7); - baseTransaction.smismember(key7, new String[] {"baz", "foo"}); - baseTransaction.sinter(new String[] {key7, key7}); - - baseTransaction.sadd(setKey2, new String[] {"a", "b"}); - baseTransaction.sunionstore(setKey3, new String[] {setKey2, key7}); - baseTransaction.sdiffstore(setKey3, new String[] {setKey2, key7}); - baseTransaction.sinterstore(setKey3, new String[] {setKey2, key7}); - baseTransaction.smove(key7, setKey2, "baz"); - - baseTransaction.zadd(key8, Map.of("one", 1.0, "two", 2.0, "three", 3.0)); - baseTransaction.zrank(key8, "one"); - baseTransaction.zaddIncr(key8, "one", 3); - baseTransaction.zrem(key8, new String[] {"one"}); - baseTransaction.zcard(key8); - baseTransaction.zmscore(key8, new String[] {"two", "three"}); - baseTransaction.zrange(key8, new RangeByIndex(0, 1)); - baseTransaction.zrangeWithScores(key8, new RangeByIndex(0, 1)); - baseTransaction.zrangestore(key8, key8, new RangeByIndex(0, -1)); - baseTransaction.zscore(key8, "two"); - baseTransaction.zcount(key8, new ScoreBoundary(2, true), InfScoreBound.POSITIVE_INFINITY); - baseTransaction.zlexcount(key8, new LexBoundary("a", true), InfLexBound.POSITIVE_INFINITY); - baseTransaction.zpopmin(key8); - baseTransaction.zpopmax(key8); - baseTransaction.zremrangebyrank(key8, 5, 10); - baseTransaction.zremrangebylex(key8, new LexBoundary("j"), InfLexBound.POSITIVE_INFINITY); - baseTransaction.zremrangebyscore(key8, new ScoreBoundary(5), InfScoreBound.POSITIVE_INFINITY); - baseTransaction.zdiffstore(key8, new String[] {key8, key8}); - - baseTransaction.zadd(zSetKey2, Map.of("one", 1.0, "two", 2.0)); - baseTransaction.zdiff(new String[] {zSetKey2, key8}); - baseTransaction.zdiffWithScores(new String[] {zSetKey2, key8}); - - baseTransaction.xadd( - key9, Map.of("field1", "value1"), StreamAddOptions.builder().id("0-1").build()); - baseTransaction.xadd( - key9, Map.of("field2", "value2"), StreamAddOptions.builder().id("0-2").build()); - baseTransaction.xadd( - key9, Map.of("field3", "value3"), StreamAddOptions.builder().id("0-3").build()); - - baseTransaction.configSet(Map.of("timeout", "1000")); - baseTransaction.configGet(new String[] {"timeout"}); - - baseTransaction.configResetStat(); - - baseTransaction.echo("GLIDE"); - - baseTransaction.rpushx(listKey3, new String[] {"_"}).lpushx(listKey3, new String[] {"_"}); - baseTransaction + + private static final String value1 = "value1-" + UUID.randomUUID(); + private static final String value2 = "value2-" + UUID.randomUUID(); + private static final String value3 = "value3-" + UUID.randomUUID(); + private static final String field1 = "field1-" + UUID.randomUUID(); + private static final String field2 = "field2-" + UUID.randomUUID(); + private static final String field3 = "field3-" + UUID.randomUUID(); + + @FunctionalInterface + public interface TransactionBuilder extends Function, Object[]> {} + + /** Generate test samples for parametrized tests. Could be routed to random node. */ + public static Stream getCommonTransactionBuilders() { + return Stream.of( + Arguments.of( + "Generic Commands", (TransactionBuilder) TransactionTestUtilities::genericCommands), + Arguments.of( + "String Commands", (TransactionBuilder) TransactionTestUtilities::stringCommands), + Arguments.of("Hash Commands", (TransactionBuilder) TransactionTestUtilities::hashCommands), + Arguments.of("List Commands", (TransactionBuilder) TransactionTestUtilities::listCommands), + Arguments.of("Set Commands", (TransactionBuilder) TransactionTestUtilities::setCommands), + Arguments.of( + "Sorted Set Commands", + (TransactionBuilder) TransactionTestUtilities::sortedSetCommands), + Arguments.of( + "HyperLogLog Commands", + (TransactionBuilder) TransactionTestUtilities::hyperLogLogCommands), + Arguments.of( + "Stream Commands", (TransactionBuilder) TransactionTestUtilities::streamCommands), + Arguments.of( + "Connection Management Commands", + (TransactionBuilder) TransactionTestUtilities::connectionManagementCommands), + Arguments.of( + "Geospatial Commands", + (TransactionBuilder) TransactionTestUtilities::geospatialCommands), + Arguments.of( + "Bitmap Commands", (TransactionBuilder) TransactionTestUtilities::bitmapCommands), + Arguments.of( + "PubSub Commands", (TransactionBuilder) TransactionTestUtilities::pubsubCommands)); + } + + /** Generate test samples for parametrized tests. Could be routed to primary nodes only. */ + public static Stream getPrimaryNodeTransactionBuilders() { + return Stream.of( + Arguments.of( + "Server Management Commands", + (TransactionBuilder) TransactionTestUtilities::serverManagementCommands), + Arguments.of( + "Scripting and Function Commands", + (TransactionBuilder) TransactionTestUtilities::scriptingAndFunctionsCommands)); + } + + private static Object[] genericCommands(BaseTransaction transaction) { + String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); + String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + String genericKey3 = "{GenericKey}-3-" + UUID.randomUUID(); + String genericKey4 = "{GenericKey}-4-" + UUID.randomUUID(); + String[] ascendingList = new String[] {"1", "2", "3"}; + String[] descendingList = new String[] {"3", "2", "1"}; + + transaction + .set(genericKey1, value1) + .customCommand(new String[] {"MGET", genericKey1, genericKey2}) + .exists(new String[] {genericKey1}) + .persist(genericKey1) + .type(genericKey1) + .objectEncoding(genericKey1) + .touch(new String[] {genericKey1}) + .set(genericKey2, value2) + .rename(genericKey1, genericKey1) + .renamenx(genericKey1, genericKey2) + .unlink(new String[] {genericKey2}) + .get(genericKey2) + .del(new String[] {genericKey1}) + .get(genericKey1) + .set(genericKey1, value1) + .expire(genericKey1, 100500) + .expireAt(genericKey1, 42) // expire (delete) key immediately + .pexpire(genericKey1, 42) + .pexpireAt(genericKey1, 42) + .ttl(genericKey2) + .lpush(genericKey3, new String[] {"3", "1", "2"}) + .sort(genericKey3) + .sortStore(genericKey3, genericKey4) + .lrange(genericKey4, 0, -1); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .set(genericKey1, value1) + .expire(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) + .expireAt(genericKey1, 500, ExpireOptions.HAS_EXISTING_EXPIRY) + .pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) + .pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) + .expiretime(genericKey1) + .pexpiretime(genericKey1) + .sortReadOnly(genericKey3); + } + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .set(genericKey3, "value") + .set(genericKey4, "value2") + .copy(genericKey3, genericKey4, false) + .copy(genericKey3, genericKey4, true); + } + + var expectedResults = + new Object[] { + OK, // set(genericKey1, value1) + new String[] {value1, null}, // customCommand("MGET", genericKey1, genericKey2) + 1L, // exists(new String[] {genericKey1}) + false, // persist(key1) + "string", // type(genericKey1) + "embstr", // objectEncoding(genericKey1) + 1L, // touch(new String[] {genericKey1}) + OK, // set(genericKey2, value2) + OK, // rename(genericKey1, genericKey1) + false, // renamenx(genericKey1, genericKey2) + 1L, // unlink(new String[] {genericKey2}) + null, // get(genericKey2) + 1L, // del(new String[] {genericKey1}) + null, // get(genericKey1) + OK, // set(genericKey1, value1) + true, // expire(genericKey1, 100500) + true, // expireAt(genericKey1, 42) + false, // pexpire(genericKey1, 42) + false, // pexpireAt(genericKey1, 42) + -2L, // ttl(genericKey2) + 3L, // lpush(genericKey3, new String[] {"3", "1", "2"}) + ascendingList, // sort(genericKey3) + 3L, // sortStore(genericKey3, genericKey4) + ascendingList, // lrange(genericKey4, 0, -1) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(genericKey1, value1) + true, // expire(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) + true, // expireAt(genericKey1, 500, ExpireOptions.HAS_EXISTING_EXPIRY) + false, // pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) + false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) + -2L, // expiretime(genericKey1) + -2L, // pexpiretime(genericKey1) + ascendingList, // sortReadOnly(genericKey3) + }); + } + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(genericKey3, "value1") + OK, // set(genericKey4, "value2") + false, // copy(genericKey3, genericKey4, false) + true, // copy(genericKey3, genericKey4, true) + }); + } + return expectedResults; + } + + private static Object[] stringCommands(BaseTransaction transaction) { + String stringKey1 = "{StringKey}-1-" + UUID.randomUUID(); + String stringKey2 = "{StringKey}-2-" + UUID.randomUUID(); + String stringKey3 = "{StringKey}-3-" + UUID.randomUUID(); + String stringKey4 = "{StringKey}-4-" + UUID.randomUUID(); + String stringKey5 = "{StringKey}-5-" + UUID.randomUUID(); + String stringKey6 = "{StringKey}-6-" + UUID.randomUUID(); + String stringKey7 = "{StringKey}-7-" + UUID.randomUUID(); + String stringKey8 = "{StringKey}-8-" + UUID.randomUUID(); + String stringKey9 = "{StringKey}-9-" + UUID.randomUUID(); + + Map expectedLcsIdxObject = + Map.of("matches", new Long[][][] {{{1L, 3L}, {0L, 2L}}}, "len", 3L); + + Map expectedLcsIdxWithMatchLenObject = + Map.of( + "matches", + new Object[] {new Object[] {new Long[] {1L, 3L}, new Long[] {0L, 2L}, 3L}}, + "len", + 3L); + + transaction + .flushall() + .set(stringKey1, value1) + .randomKey() + .get(stringKey1) + .getdel(stringKey1) + .set(stringKey2, value2, SetOptions.builder().returnOldValue(true).build()) + .strlen(stringKey2) + .append(stringKey2, value2) + .mset(Map.of(stringKey1, value2, stringKey2, value1)) + .mget(new String[] {stringKey1, stringKey2}) + .incr(stringKey3) + .incrBy(stringKey3, 2) + .decr(stringKey3) + .decrBy(stringKey3, 2) + .incrByFloat(stringKey3, 0.5) + .setrange(stringKey3, 0, "GLIDE") + .getrange(stringKey3, 0, 5) + .msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + .mget(new String[] {stringKey4, stringKey5}) + .del(new String[] {stringKey5}) + .msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + .mget(new String[] {stringKey4, stringKey5}); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .set(stringKey6, "abcd") + .set(stringKey7, "bcde") + .set(stringKey8, "wxyz") + .lcs(stringKey6, stringKey7) + .lcs(stringKey6, stringKey8) + .lcsLen(stringKey6, stringKey7) + .lcsLen(stringKey6, stringKey8) + .lcsIdx(stringKey6, stringKey7) + .lcsIdx(stringKey6, stringKey7, 1) + .lcsIdxWithMatchLen(stringKey6, stringKey7) + .lcsIdxWithMatchLen(stringKey6, stringKey7, 1); + } + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .set(stringKey9, value1) + .getex(stringKey9) + .getex(stringKey9, GetExOptions.Seconds(20L)); + } + + var expectedResults = + new Object[] { + OK, // flushall() + OK, // set(stringKey1, value1) + stringKey1, // randomKey() + value1, // get(stringKey1) + value1, // getdel(stringKey1) + null, // set(stringKey2, value2, returnOldValue(true)) + (long) value1.length(), // strlen(key2) + Long.valueOf(value2.length() * 2), // append(key2, value2) + OK, // mset(Map.of(stringKey1, value2, stringKey2, value1)) + new String[] {value2, value1}, // mget(new String[] {stringKey1, stringKey2}) + 1L, // incr(stringKey3) + 3L, // incrBy(stringKey3, 2) + 2L, // decr(stringKey3) + 0L, // decrBy(stringKey3, 2) + 0.5, // incrByFloat(stringKey3, 0.5) + 5L, // setrange(stringKey3, 0, "GLIDE") + "GLIDE", // getrange(stringKey3, 0, 5) + true, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + new String[] {"foo", "bar"}, // mget({stringKey4, stringKey5}) + 1L, // del(stringKey5) + false, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + new String[] {"foo", null}, // mget({stringKey4, stringKey5}) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(stringKey6, "abcd") + OK, // set(stringKey6, "bcde") + OK, // set(stringKey6, "wxyz") + "bcd", // lcs(stringKey6, stringKey7) + "", // lcs(stringKey6, stringKey8) + 3L, // lcsLEN(stringKey6, stringKey7) + 0L, // lcsLEN(stringKey6, stringKey8) + expectedLcsIdxObject, // lcsIdx(stringKey6, stringKey7) + expectedLcsIdxObject, // lcsIdx(stringKey6, stringKey7, minMatchLen(1L) + expectedLcsIdxWithMatchLenObject, // lcsIdxWithMatchLen(stringKey6, stringKey7) + expectedLcsIdxWithMatchLenObject, // lcsIdxWithMatchLen(key6, key7, minMatchLen(1L)) + }); + } + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(stringKey9, value1) + value1, // getex(stringKey1) + value1, // getex(stringKey1,GetExOptions.Seconds(20L)) + }); + } + + return expectedResults; + } + + private static Object[] hashCommands(BaseTransaction transaction) { + String hashKey1 = "{HashKey}-1-" + UUID.randomUUID(); + + // This extra key is for HScan testing. It is a key with only one field. HScan doesn't guarantee + // a return order but this test compares arrays so order is significant. + String hashKey2 = "{HashKey}-2-" + UUID.randomUUID(); + + transaction + .hset(hashKey1, Map.of(field1, value1, field2, value2)) + .hget(hashKey1, field1) + .hlen(hashKey1) + .hexists(hashKey1, field2) + .hsetnx(hashKey1, field1, value1) + .hmget(hashKey1, new String[] {field1, "non_existing_field", field2}) + .hgetall(hashKey1) + .hdel(hashKey1, new String[] {field1}) + .hvals(hashKey1) + .hrandfield(hashKey1) + .hrandfieldWithCount(hashKey1, 2) + .hrandfieldWithCount(hashKey1, -2) + .hrandfieldWithCountWithValues(hashKey1, 2) + .hrandfieldWithCountWithValues(hashKey1, -2) + .hincrBy(hashKey1, field3, 5) + .hincrByFloat(hashKey1, field3, 5.5) + .hkeys(hashKey1) + .hstrlen(hashKey1, field2) + .hset(hashKey2, Map.of(field1, value1)) + .hscan(hashKey2, "0") + .hscan(hashKey2, "0", HScanOptions.builder().count(20L).build()); + + return new Object[] { + 2L, // hset(hashKey1, Map.of(field1, value1, field2, value2)) + value1, // hget(hashKey1, field1) + 2L, // hlen(hashKey1) + true, // hexists(hashKey1, field2) + false, // hsetnx(hashKey1, field1, value1) + new String[] {value1, null, value2}, // hmget(hashKey1, new String[] {...}) + Map.of(field1, value1, field2, value2), // hgetall(hashKey1) + 1L, // hdel(hashKey1, new String[] {field1}) + new String[] {value2}, // hvals(hashKey1) + field2, // hrandfield(hashKey1) + new String[] {field2}, // hrandfieldWithCount(hashKey1, 2) + new String[] {field2, field2}, // hrandfieldWithCount(hashKey1, -2) + new String[][] {{field2, value2}}, // hrandfieldWithCountWithValues(hashKey1, 2) + new String[][] { + {field2, value2}, {field2, value2} + }, // hrandfieldWithCountWithValues(hashKey1, -2) + 5L, // hincrBy(hashKey1, field3, 5) + 10.5, // hincrByFloat(hashKey1, field3, 5.5) + new String[] {field2, field3}, // hkeys(hashKey1) + (long) value2.length(), // hstrlen(hashKey1, field2) + 1L, // hset(hashKey2, Map.of(field1, value1)) + new Object[] {"0", new Object[] {field1, value1}}, // hscan(hashKey2, "0") + new Object[] { + "0", new Object[] {field1, value1} + }, // hscan(hashKey2, "0", HScanOptions.builder().count(20L).build()); + }; + } + + private static Object[] listCommands(BaseTransaction transaction) { + String listKey1 = "{ListKey}-1-" + UUID.randomUUID(); + String listKey2 = "{ListKey}-2-" + UUID.randomUUID(); + String listKey3 = "{ListKey}-3-" + UUID.randomUUID(); + String listKey4 = "{ListKey}-4-" + UUID.randomUUID(); + String listKey5 = "{ListKey}-5-" + UUID.randomUUID(); + String listKey6 = "{ListKey}-6-" + UUID.randomUUID(); + String listKey7 = "{ListKey}-7-" + UUID.randomUUID(); + + transaction + .lpush(listKey1, new String[] {value1, value1, value2, value3, value3}) + .llen(listKey1) + .lindex(listKey1, 0) + .lrem(listKey1, 1, value1) + .ltrim(listKey1, 1, -1) + .lrange(listKey1, 0, -2) + .lpop(listKey1) + .lpopCount(listKey1, 2) // listKey1 is now empty + .rpush(listKey1, new String[] {value1, value1, value2, value3, value3}) + .lpos(listKey1, value1) + .lpos(listKey1, value1, LPosOptions.builder().rank(2L).build()) + .lposCount(listKey1, value1, 1L) + .lposCount(listKey1, value1, 0L, LPosOptions.builder().rank(1L).build()) + .rpush(listKey2, new String[] {value1, value2, value2}) + .rpop(listKey2) + .rpopCount(listKey2, 2) + .rpushx(listKey3, new String[] {"_"}) + .lpushx(listKey3, new String[] {"_"}) .lpush(listKey3, new String[] {value1, value2, value3}) - .linsert(listKey3, AFTER, value2, value2); + .linsert(listKey3, AFTER, value2, value2) + .blpop(new String[] {listKey3}, 0.01) + .brpop(new String[] {listKey3}, 0.01) + .lpush(listKey5, new String[] {value2, value3}) + .lset(listKey5, 0, value1) + .lrange(listKey5, 0, -1); - baseTransaction.blpop(new String[] {listKey3}, 0.01).brpop(new String[] {listKey3}, 0.01); + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .lpush(listKey4, new String[] {value1, value2, value3, value1, value2, value3}) + .lmpop(new String[] {listKey4}, ListDirection.LEFT) + .lmpop(new String[] {listKey4}, ListDirection.LEFT, 2L) + .blmpop(new String[] {listKey4}, ListDirection.LEFT, 0.1) + .blmpop(new String[] {listKey4}, ListDirection.LEFT, 2L, 0.1); + } // listKey4 is now empty - baseTransaction.pfadd(hllKey1, new String[] {"a", "b", "c"}); - baseTransaction.pfcount(new String[] {hllKey1, hllKey2}); - baseTransaction - .pfmerge(hllKey3, new String[] {hllKey1, hllKey2}) - .pfcount(new String[] {hllKey3}); + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .lpush(listKey6, new String[] {value3, value2, value1}) + .lpush(listKey7, new String[] {value1, value2, value3}) + .lmove(listKey7, listKey7, ListDirection.LEFT, ListDirection.LEFT) + .lmove(listKey6, listKey7, ListDirection.LEFT, ListDirection.RIGHT) + .lrange(listKey6, 0, -1) + .lrange(listKey7, 0, -1) + .blmove(listKey7, listKey7, ListDirection.LEFT, ListDirection.LEFT, 0.1) + .blmove(listKey7, listKey6, ListDirection.RIGHT, ListDirection.LEFT, 0.1) + .lrange(listKey6, 0, -1) + .lrange(listKey7, 0, -1); + } + + var expectedResults = + new Object[] { + 5L, // lpush(listKey1, new String[] {value1, value1, value2, value3, value3}) + 5L, // llen(listKey1) + value3, // lindex(key5, 0) + 1L, // lrem(listKey1, 1, value1) + OK, // ltrim(listKey1, 1, -1) + new String[] {value3, value2}, // lrange(listKey1, 0, -2) + value3, // lpop(listKey1) + new String[] {value2, value1}, // lpopCount(listKey1, 2) + 5L, // lpush(listKey1, new String[] {value1, value1, value2, value3, value3}) + 0L, // lpos(listKey1, value1) + 1L, // lpos(listKey1, value1, LPosOptions.builder().rank(2L).build()) + new Long[] {0L}, // lposCount(listKey1, value1, 1L) + new Long[] {0L, 1L}, // lposCount(listKey1, value1, 0L, LPosOptions.rank(2L)) + 3L, // rpush(listKey2, new String[] {value1, value2, value2}) + value2, // rpop(listKey2) + new String[] {value2, value1}, // rpopCount(listKey2, 2) + 0L, // rpushx(listKey3, new String[] { "_" }) + 0L, // lpushx(listKey3, new String[] { "_" }) + 3L, // lpush(listKey3, new String[] { value1, value2, value3}) + 4L, // linsert(listKey3, AFTER, value2, value2) + new String[] {listKey3, value3}, // blpop(new String[] { listKey3 }, 0.01) + new String[] {listKey3, value1}, // brpop(new String[] { listKey3 }, 0.01) + 2L, // lpush(listKey5, new String[] {value2, value3}) + OK, // lset(listKey5, 0, value1) + new String[] {value1, value2}, // lrange(listKey5, 0, -1) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + 6L, // lpush(listKey4, {value1, value2, value3}) + Map.of(listKey4, new String[] {value3}), // lmpop({listKey4}, LEFT) + Map.of(listKey4, new String[] {value2, value1}), // lmpop({listKey4}, LEFT, 1L) + Map.of(listKey4, new String[] {value3}), // blmpop({listKey4}, LEFT, 0.1) + Map.of(listKey4, new String[] {value2, value1}), // blmpop(listKey4}, LEFT, 1L, 0.1) + }); + } + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + 3L, // lpush(listKey6, {value3, value2, value1}) + 3L, // lpush(listKey7, {value1, value2, value3}) + value3, // lmove(listKey7, listKey5, LEFT, LEFT) + value1, // lmove(listKey6, listKey5, RIGHT, LEFT) + new String[] {value2, value3}, // lrange(listKey6, 0, -1) + new String[] {value3, value2, value1, value1}, // lrange(listKey7, 0, -1); + value3, // blmove(listKey7, listKey7, LEFT, LEFT, 0.1) + value1, // blmove(listKey7, listKey6, RIGHT, LEFT, 0.1) + new String[] {value1, value2, value3}, // lrange(listKey6, 0, -1) + new String[] {value3, value2, value1}, // lrange(listKey7, 0, -1) + }); + } + + return expectedResults; + } + + private static Object[] setCommands(BaseTransaction transaction) { + String setKey1 = "{setKey}-1-" + UUID.randomUUID(); + String setKey2 = "{setKey}-2-" + UUID.randomUUID(); + String setKey3 = "{setKey}-3-" + UUID.randomUUID(); + String setKey4 = "{setKey}-4-" + UUID.randomUUID(); + String setKey5 = "{setKey}-5-" + UUID.randomUUID(); + String setKey6 = "{setKey}-6-" + UUID.randomUUID(); + + transaction + .sadd(setKey1, new String[] {"baz", "foo"}) + .srem(setKey1, new String[] {"foo"}) + .sscan(setKey1, "0") + .sscan(setKey1, "0", SScanOptions.builder().matchPattern("*").count(10L).build()) + .scard(setKey1) + .sismember(setKey1, "baz") + .smembers(setKey1) + .smismember(setKey1, new String[] {"baz", "foo"}) + .sinter(new String[] {setKey1, setKey1}) + .sadd(setKey2, new String[] {"a", "b"}) + .sunion(new String[] {setKey2, setKey1}) + .sunionstore(setKey3, new String[] {setKey2, setKey1}) + .sdiffstore(setKey3, new String[] {setKey2, setKey1}) + .sinterstore(setKey3, new String[] {setKey2, setKey1}) + .sdiff(new String[] {setKey2, setKey3}) + .smove(setKey1, setKey2, "baz") + .sadd(setKey4, new String[] {"foo"}) + .srandmember(setKey4) + .srandmember(setKey4, 2) + .srandmember(setKey4, -2) + .spop(setKey4) + .spopCount(setKey4, 3); // setKey4 is now empty + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .sadd(setKey5, new String[] {"one", "two", "three", "four"}) + .sadd(setKey6, new String[] {"two", "three", "four", "five"}) + .sintercard(new String[] {setKey5, setKey6}) + .sintercard(new String[] {setKey5, setKey6}, 2); + } + + var expectedResults = + new Object[] { + 2L, // sadd(setKey1, new String[] {"baz", "foo"}); + 1L, // srem(setKey1, new String[] {"foo"}); + new Object[] {"0", new String[] {"baz"}}, // sscan(setKey1, "0") + new Object[] {"0", new String[] {"baz"}}, // sscan(key1, "0", match "*", count(10L)) + 1L, // scard(setKey1); + true, // sismember(setKey1, "baz") + Set.of("baz"), // smembers(setKey1); + new Boolean[] {true, false}, // smismembmer(setKey1, new String[] {"baz", "foo"}) + Set.of("baz"), // sinter(new String[] { setKey1, setKey1 }) + 2L, // sadd(setKey2, new String[] { "a", "b" }) + Set.of("a", "b", "baz"), // sunion(new String[] {setKey2, setKey1}) + 3L, // sunionstore(setKey3, new String[] { setKey2, setKey1 }) + 2L, // sdiffstore(setKey3, new String[] { setKey2, setKey1 }) + 0L, // sinterstore(setKey3, new String[] { setKey2, setKey1 }) + Set.of("a", "b"), // sdiff(new String[] {setKey2, setKey3}) + true, // smove(setKey1, setKey2, "baz") + 1L, // sadd(setKey4, {"foo}) + "foo", // srandmember(setKey4) + new String[] {"foo"}, // srandmember(setKey4, 2) + new String[] {"foo", "foo"}, // srandmember(setKey4, -2)}; + "foo", // spop(setKey4) + Set.of(), // spopCount(setKey4, 3) + }; + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + 4L, // sadd(setKey5, {"one", "two", "three", "four"}) + 4L, // sadd(setKey6, {"two", "three", "four", "five"}) + 3L, // sintercard({setKey5, setKey6}) + 2L, // sintercard({setKey5, setKey6}, 2) + }); + } + + return expectedResults; + } + + private static Object[] sortedSetCommands(BaseTransaction transaction) { + String zSetKey1 = "{ZSetKey}-1-" + UUID.randomUUID(); + String zSetKey2 = "{ZSetKey}-2-" + UUID.randomUUID(); + String zSetKey3 = "{ZSetKey}-3-" + UUID.randomUUID(); + String zSetKey4 = "{ZSetKey}-4-" + UUID.randomUUID(); + String zSetKey5 = "{ZSetKey}-4-" + UUID.randomUUID(); + String zSetKey6 = "{ZSetKey}-5-" + UUID.randomUUID(); + + transaction + .zadd(zSetKey1, Map.of("one", 1.0, "two", 2.0, "three", 3.0)) + .zrank(zSetKey1, "one") + .zrevrank(zSetKey1, "one") + .zaddIncr(zSetKey1, "one", 3) + .zincrby(zSetKey1, -3., "one") + .zrem(zSetKey1, new String[] {"one"}) + .zcard(zSetKey1) + .zmscore(zSetKey1, new String[] {"two", "three"}) + .zrange(zSetKey1, new RangeByIndex(0, 1)) + .zrangeWithScores(zSetKey1, new RangeByIndex(0, 1)) + .zrangestore(zSetKey1, zSetKey1, new RangeByIndex(0, -1)) + .zscore(zSetKey1, "two") + .zcount(zSetKey1, new ScoreBoundary(2, true), InfScoreBound.POSITIVE_INFINITY) + .zlexcount(zSetKey1, new LexBoundary("a", true), InfLexBound.POSITIVE_INFINITY) + .zpopmin(zSetKey1) + .zpopmax(zSetKey1) + // zSetKey1 is now empty + .zremrangebyrank(zSetKey1, 5, 10) + .zremrangebylex(zSetKey1, new LexBoundary("j"), InfLexBound.POSITIVE_INFINITY) + .zremrangebyscore(zSetKey1, new ScoreBoundary(5), InfScoreBound.POSITIVE_INFINITY) + .zadd(zSetKey2, Map.of("one", 1.0, "two", 2.0)) + .bzpopmax(new String[] {zSetKey2}, .1) + .zrandmember(zSetKey2) + .zrandmemberWithCount(zSetKey2, 1) + .zrandmemberWithCountWithScores(zSetKey2, 1) + .zscan(zSetKey2, "0") + .zscan(zSetKey2, "0", ZScanOptions.builder().count(20L).build()) + .bzpopmin(new String[] {zSetKey2}, .1); + // zSetKey2 is now empty + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .zadd(zSetKey5, Map.of("one", 1.0, "two", 2.0)) + // zSetKey6 is empty + .zdiffstore(zSetKey6, new String[] {zSetKey6, zSetKey6}) + .zdiff(new String[] {zSetKey5, zSetKey6}) + .zdiffWithScores(new String[] {zSetKey5, zSetKey6}) + .zunionstore(zSetKey5, new KeyArray(new String[] {zSetKey5, zSetKey6})) + .zunion(new KeyArray(new String[] {zSetKey5, zSetKey6})) + .zunionWithScores(new KeyArray(new String[] {zSetKey5, zSetKey6})) + .zunionWithScores(new KeyArray(new String[] {zSetKey5, zSetKey6}), Aggregate.MAX) + .zinterstore(zSetKey6, new KeyArray(new String[] {zSetKey5, zSetKey6})) + .zinter(new KeyArray(new String[] {zSetKey5, zSetKey6})) + .zinterWithScores(new KeyArray(new String[] {zSetKey5, zSetKey6})) + .zinterWithScores(new KeyArray(new String[] {zSetKey5, zSetKey6}), Aggregate.MAX); + } + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .zadd(zSetKey3, Map.of("a", 1., "b", 2., "c", 3., "d", 4., "e", 5., "f", 6., "g", 7.)) + .zadd(zSetKey4, Map.of("a", 1., "b", 2., "c", 3., "d", 4.)) + .zmpop(new String[] {zSetKey3}, MAX) + .zmpop(new String[] {zSetKey3}, MIN, 2) + .bzmpop(new String[] {zSetKey3}, MAX, .1) + .bzmpop(new String[] {zSetKey3}, MIN, .1, 2) + .zadd(zSetKey3, Map.of("a", 1., "b", 2., "c", 3., "d", 4., "e", 5., "f", 6., "g", 7.)) + .zintercard(new String[] {zSetKey4, zSetKey3}) + .zintercard(new String[] {zSetKey4, zSetKey3}, 2); + } + + var expectedResults = + new Object[] { + 3L, // zadd(zSetKey1, Map.of("one", 1.0, "two", 2.0, "three", 3.0)) + 0L, // zrank(zSetKey1, "one") + 2L, // zrevrank(zSetKey1, "one") + 4.0, // zaddIncr(zSetKey1, "one", 3) + 1., // zincrby(zSetKey1, -3.3, "one") + 1L, // zrem(zSetKey1, new String[] {"one"}) + 2L, // zcard(zSetKey1) + new Double[] {2.0, 3.0}, // zmscore(zSetKey1, new String[] {"two", "three"}) + new String[] {"two", "three"}, // zrange(zSetKey1, new RangeByIndex(0, 1)) + Map.of("two", 2.0, "three", 3.0), // zrangeWithScores(zSetKey1, new RangeByIndex(0, 1)) + 2L, // zrangestore(zSetKey1, zSetKey1, new RangeByIndex(0, -1)) + 2.0, // zscore(zSetKey1, "two") + 2L, // zcount(zSetKey1, new ScoreBoundary(2, true), InfScoreBound.POSITIVE_INFINITY) + 2L, // zlexcount(zSetKey1, new LexBoundary("a", true), InfLexBound.POSITIVE_INFINITY) + Map.of("two", 2.0), // zpopmin(zSetKey1) + Map.of("three", 3.0), // zpopmax(zSetKey1) + 0L, // zremrangebyrank(zSetKey1, 5, 10) + 0L, // zremrangebylex(zSetKey1, new LexBoundary("j"), InfLexBound.POSITIVE_INFINITY) + 0L, // zremrangebyscore(zSetKey1, new ScoreBoundary(5), InfScoreBound.POSITIVE_INFINITY) + 2L, // zadd(zSetKey2, Map.of("one", 1.0, "two", 2.0)) + new Object[] {zSetKey2, "two", 2.0}, // bzpopmax(new String[] { zsetKey2 }, .1) + "one", // zrandmember(zSetKey2) + new String[] {"one"}, // .zrandmemberWithCount(zSetKey2, 1) + new Object[][] {{"one", 1.0}}, // .zrandmemberWithCountWithScores(zSetKey2, 1); + new Object[] {"0", new String[] {"one", "1"}}, // zscan(zSetKey2, 0) + new Object[] { + "0", new String[] {"one", "1"} + }, // zscan(zSetKey2, 0, ZScanOptions.builder().count(20L).build()) + new Object[] {zSetKey2, "one", 1.0}, // bzpopmin(new String[] { zsetKey2 }, .1) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + 2L, // zadd(zSetKey5, Map.of("one", 1.0, "two", 2.0)) + 0L, // zdiffstore(zSetKey6, new String[] {zSetKey6, zSetKey6}) + new String[] {"one", "two"}, // zdiff(new String[] {zSetKey5, zSetKey6}) + Map.of("one", 1.0, "two", 2.0), // zdiffWithScores({zSetKey5, zSetKey6}) + 2L, // zunionstore(zSetKey5, new KeyArray(new String[] {zSetKey5, zSetKey6})) + new String[] {"one", "two"}, // zunion(new KeyArray({zSetKey5, zSetKey6})) + Map.of("one", 1.0, "two", 2.0), // zunionWithScores({zSetKey5, zSetKey6}) + Map.of("one", 1.0, "two", 2.0), // zunionWithScores({zSetKey5, zSetKey6}, MAX) + 0L, // zinterstore(zSetKey6, new String[] {zSetKey5, zSetKey6}) + new String[0], // zinter(new KeyArray({zSetKey5, zSetKey6})) + Map.of(), // zinterWithScores(new KeyArray({zSetKey5, zSetKey6})) + Map.of(), // zinterWithScores(new KeyArray({zSetKey5, zSetKey6}), Aggregate.MAX) + }); + } - return baseTransaction; + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + 7L, // zadd(zSetKey3, "a", 1., "b", 2., "c", 3., "d", 4., "e", 5., "f", 6., "g", 7.) + 4L, // zadd(zSetKey4, Map.of("a", 1., "b", 2., "c", 3., "d", 4.)) + new Object[] {zSetKey3, Map.of("g", 7.)}, // zmpop(zSetKey3, MAX) + new Object[] {zSetKey3, Map.of("a", 1., "b", 2.)}, // zmpop(zSetKey3, MIN, 2) + new Object[] {zSetKey3, Map.of("f", 6.)}, // bzmpop(zSetKey3, MAX, .1) + new Object[] {zSetKey3, Map.of("c", 3., "d", 4.)}, // bzmpop(zSetKey3, MIN, .1, 2) + 6L, // zadd(zSetKey3, "a", 1., "b", 2., "c", 3., "d", 4., "e", 5., "f", 6., "g", 7.) + 4L, // zintercard(new String[] {zSetKey4, zSetKey3}) + 2L, // zintercard(new String[] {zSetKey4, zSetKey3}, 2) + }); + } + return expectedResults; } - public static Object[] transactionTestResult() { + private static Object[] serverManagementCommands(BaseTransaction transaction) { + transaction + .configSet(Map.of("timeout", "1000")) + .configGet(new String[] {"timeout"}) + .configResetStat() + .lolwut(1) + .flushall() + .flushall(ASYNC) + .flushdb() + .flushdb(ASYNC) + .dbsize(); + + return new Object[] { + OK, // configSet(Map.of("timeout", "1000")) + Map.of("timeout", "1000"), // configGet(new String[] {"timeout"}) + OK, // configResetStat() + "Redis ver. " + SERVER_VERSION + '\n', // lolwut(1) + OK, // flushall() + OK, // flushall(ASYNC) + OK, // flushdb() + OK, // flushdb(ASYNC) + 0L, // dbsize() + }; + } + + private static Object[] connectionManagementCommands(BaseTransaction transaction) { + transaction.ping().ping(value1).echo(value2); + // untested: + // clientId + // clientGetName + + return new Object[] { + "PONG", // ping() + value1, // ping(value1) + value2, // echo(value2) + }; + } + + private static Object[] hyperLogLogCommands(BaseTransaction transaction) { + String hllKey1 = "{HllKey}-1-" + UUID.randomUUID(); + String hllKey2 = "{HllKey}-2-" + UUID.randomUUID(); + String hllKey3 = "{HllKey}-3-" + UUID.randomUUID(); + + transaction + .pfadd(hllKey1, new String[] {"a", "b", "c"}) + .pfcount(new String[] {hllKey1, hllKey2}) + .pfmerge(hllKey3, new String[] {hllKey1, hllKey2}) + .pfcount(new String[] {hllKey3}); + return new Object[] { - OK, - value1, - "string", // type(key1) - null, - (long) value1.length(), // strlen(key2) - new String[] {value1, value2}, - 1L, - Boolean.FALSE, // persist(key1) - 1L, - null, - 1L, - null, - OK, - new String[] {value2, value1}, - 1L, - 3L, - 2L, - 0L, - 0.5, - 1L, - 5L, // setrange(key3, 0, "GLIDE") - "GLIDE", // getrange(key3, 0, 5) - 2L, - value1, - 2L, // hlen(key4) - true, - Boolean.FALSE, // hsetnx(key4, field1, value1) - new String[] {value1, null, value2}, - Map.of(field1, value1, field2, value2), - 1L, - new String[] {value2}, // hvals(key4) - 5L, - 10.5, - 5L, - 5L, - value3, // lindex(key5, 0) - 1L, - OK, - new String[] {value3, value2}, - value3, - new String[] {value2, value1}, - 3L, - value2, - new String[] {value2, value1}, - 2L, - 1L, - 1L, - true, // sismember(key7, "baz") - Set.of("baz"), // smembers(key7) - new Boolean[] {true, false}, // smismembmer(key7, new String[] {"baz", "foo"}) - Set.of("baz"), // sinter(new String[] { key7, key7 }) - 2L, // sadd(setKey2, new String[] { "a", "b" }) - 3L, // sunionstore(setKey3, new String[] { setKey2, key7 }) - 2L, // sdiffstore(setKey3, new String[] { setKey2, key7 }) - 0L, // sinterstore(setKey3, new String[] { setKey2, key7 }) - true, // smove(key7, setKey2, "baz") - 3L, - 0L, // zrank(key8, "one") - 4.0, - 1L, - 2L, - new Double[] {2.0, 3.0}, // zmscore(key8, new String[] {"two", "three"}) - new String[] {"two", "three"}, // zrange - Map.of("two", 2.0, "three", 3.0), // zrangeWithScores - 2L, // zrangestore(key8, key8, new RangeByIndex(0, -1)) - 2.0, // zscore(key8, "two") - 2L, // zcount(key8, new ScoreBoundary(2, true), InfScoreBound.POSITIVE_INFINITY) - 2L, // zlexcount(key8, new LexBoundary("a", true), InfLexBound.POSITIVE_INFINITY) - Map.of("two", 2.0), // zpopmin(key8) - Map.of("three", 3.0), // zpopmax(key8) - 0L, // zremrangebyrank(key8, 5, 10) - 0L, // zremrangebylex(key8, new LexBoundary("j"), InfLexBound.POSITIVE_INFINITY) - 0L, // zremrangebyscore(key8, new ScoreBoundary(5), InfScoreBound.POSITIVE_INFINITY) - 0L, // zdiffstore(key8, new String[] {key8, key8}) - 2L, // zadd(zSetKey2, Map.of("one", 1.0, "two", 2.0)) - new String[] {"one", "two"}, // zdiff(new String[] {zSetKey2, key8}) - Map.of("one", 1.0, "two", 2.0), // zdiffWithScores(new String[] {zSetKey2, key8}) - "0-1", // xadd(key9, Map.of("field1", "value1"), - // StreamAddOptions.builder().id("0-1").build()); - "0-2", // xadd(key9, Map.of("field2", "value2"), - // StreamAddOptions.builder().id("0-2").build()); - "0-3", // xadd(key9, Map.of("field3", "value3"), - // StreamAddOptions.builder().id("0-3").build()); - OK, - Map.of("timeout", "1000"), - OK, - "GLIDE", // echo - 0L, // rpushx(listKey3, new String[] { "_" }) - 0L, // lpushx(listKey3, new String[] { "_" }) - 3L, // lpush(listKey3, new String[] { value1, value2, value3}) - 4L, // linsert(listKey3, AFTER, value2, value2) - new String[] {listKey3, value3}, // blpop(new String[] { listKey3 }, 0.01) - new String[] {listKey3, value1}, // brpop(new String[] { listKey3 }, 0.01); 1L, // pfadd(hllKey1, new String[] {"a", "b", "c"}) - 3L, // pfcount(new String[] { hllKey1, hllKey2 });; + 3L, // pfcount(new String[] { hllKey1, hllKey2 }) OK, // pfmerge(hllKey3, new String[] {hllKey1, hllKey2}) 3L, // pfcount(new String[] { hllKey3 }) }; } + + private static Object[] streamCommands(BaseTransaction transaction) { + final String streamKey1 = "{streamKey}-1-" + UUID.randomUUID(); + final String streamKey2 = "{streamKey}-2-" + UUID.randomUUID(); + final String streamKey3 = "{streamKey}-3-" + UUID.randomUUID(); + final String groupName1 = "{groupName}-1-" + UUID.randomUUID(); + final String groupName2 = "{groupName}-2-" + UUID.randomUUID(); + final String groupName3 = "{groupName}-2-" + UUID.randomUUID(); + final String consumer1 = "{consumer}-1-" + UUID.randomUUID(); + + transaction + .xadd(streamKey1, Map.of("field1", "value1"), StreamAddOptions.builder().id("0-1").build()) + .xadd(streamKey1, Map.of("field2", "value2"), StreamAddOptions.builder().id("0-2").build()) + .xadd(streamKey1, Map.of("field3", "value3"), StreamAddOptions.builder().id("0-3").build()) + .xlen(streamKey1) + .xread(Map.of(streamKey1, "0-2")) + .xread(Map.of(streamKey1, "0-2"), StreamReadOptions.builder().count(1L).build()) + .xrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1")) + .xrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1"), 1L) + .xrevrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1")) + .xrevrange(streamKey1, IdBound.of("0-1"), IdBound.of("0-1"), 1L) + .xtrim(streamKey1, new MinId(true, "0-2")) + .xgroupCreate(streamKey1, groupName1, "0-2") + .xinfoConsumers(streamKey1, groupName1) + .xgroupCreate( + streamKey1, groupName2, "0-0", StreamGroupOptions.builder().makeStream().build()) + .xgroupCreateConsumer(streamKey1, groupName1, consumer1) + .xgroupSetId(streamKey1, groupName1, "0-2") + .xreadgroup(Map.of(streamKey1, ">"), groupName1, consumer1) + .xreadgroup( + Map.of(streamKey1, "0-3"), + groupName1, + consumer1, + StreamReadGroupOptions.builder().count(2L).build()) + .xclaim(streamKey1, groupName1, consumer1, 0L, new String[] {"0-1"}) + .xclaim( + streamKey1, + groupName1, + consumer1, + 0L, + new String[] {"0-3"}, + StreamClaimOptions.builder().force().build()) + .xclaimJustId(streamKey1, groupName1, consumer1, 0L, new String[] {"0-3"}) + .xclaimJustId( + streamKey1, + groupName1, + consumer1, + 0L, + new String[] {"0-4"}, + StreamClaimOptions.builder().force().build()) + .xpending(streamKey1, groupName1); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .xautoclaim(streamKey1, groupName1, consumer1, 0L, "0-0") + .xautoclaimJustId(streamKey1, groupName1, consumer1, 0L, "0-0"); + } + + transaction + .xack(streamKey1, groupName1, new String[] {"0-3"}) + .xpending( + streamKey1, + groupName1, + StreamRange.InfRangeBound.MIN, + StreamRange.InfRangeBound.MAX, + 1L) + .xgroupDelConsumer(streamKey1, groupName1, consumer1) + .xgroupDestroy(streamKey1, groupName1) + .xgroupDestroy(streamKey1, groupName2) + .xdel(streamKey1, new String[] {"0-3", "0-5"}) + .xadd(streamKey3, Map.of("f0", "v0"), StreamAddOptions.builder().id("1-0").build()) + .xgroupCreate(streamKey3, groupName3, "0") + .xinfoGroups(streamKey1); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .xadd(streamKey2, Map.of("f0", "v0"), StreamAddOptions.builder().id("1-0").build()) + .xgroupCreate(streamKey2, groupName3, "0") + .xgroupSetId(streamKey2, groupName3, "1-0", 1); + } + + var result = + new Object[] { + "0-1", // xadd(streamKey1, Map.of("field1", "value1"), ... .id("0-1").build()); + "0-2", // xadd(streamKey1, Map.of("field2", "value2"), ... .id("0-2").build()); + "0-3", // xadd(streamKey1, Map.of("field3", "value3"), ... .id("0-3").build()); + 3L, // xlen(streamKey1) + Map.of( + streamKey1, + Map.of("0-3", new String[][] {{"field3", "value3"}})), // xread(Map.of(key9, "0-2")); + Map.of( + streamKey1, + Map.of( + "0-3", + new String[][] {{"field3", "value3"}})), // xread(Map.of(key9, "0-2"), options); + Map.of("0-1", new String[][] {{"field1", "value1"}}), // .xrange(streamKey1, "0-1", "0-1") + Map.of( + "0-1", + new String[][] {{"field1", "value1"}}), // .xrange(streamKey1, "0-1", "0-1", 1l) + Map.of( + "0-1", new String[][] {{"field1", "value1"}}), // .xrevrange(streamKey1, "0-1", "0-1") + Map.of( + "0-1", + new String[][] {{"field1", "value1"}}), // .xrevrange(streamKey1, "0-1", "0-1", 1l) + 1L, // xtrim(streamKey1, new MinId(true, "0-2")) + OK, // xgroupCreate(streamKey1, groupName1, "0-0") + new Map[] {}, // .xinfoConsumers(streamKey1, groupName1) + OK, // xgroupCreate(streamKey1, groupName1, "0-0", options) + true, // xgroupCreateConsumer(streamKey1, groupName1, consumer1) + OK, // xgroupSetId(streamKey1, groupName1, "0-2") + Map.of( + streamKey1, + Map.of( + "0-3", + new String[][] { + {"field3", "value3"} + })), // xreadgroup(Map.of(streamKey1, ">"), groupName1, consumer1); + Map.of( + streamKey1, + Map.of()), // xreadgroup(Map.of(streamKey1, ">"), groupName1, consumer1, options); + Map.of(), // xclaim(streamKey1, groupName1, consumer1, 0L, new String[] {"0-1"}) + Map.of( + "0-3", + new String[][] {{"field3", "value3"}}), // xclaim(streamKey1, ..., {"0-3"}, options) + new String[] {"0-3"}, // xclaimJustId(streamKey1, ..., new String[] {"0-3"}) + new String[0], // xclaimJustId(streamKey1, ..., new String[] {"0-4"}, options) + new Object[] { + 1L, "0-3", "0-3", new Object[][] {{consumer1, "1"}} // xpending(streamKey1, groupName1) + } + }; + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + result = + concatenateArrays( + result, + new Object[] { + new Object[] { + "0-0", + Map.of("0-3", new String[][] {{"field3", "value3"}}), + new Object[] {} // one more array is returned here for version >= 7.0.0 + }, // xautoclaim(streamKey1, groupName1, consumer1, 0L, "0-0") + new Object[] { + "0-0", + new String[] {"0-3"}, + new Object[] {} // one more array is returned here for version >= 7.0.0 + } // xautoclaimJustId(streamKey1, groupName1, consumer1, 0L, "0-0"); + }); + } else if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + result = + concatenateArrays( + result, + new Object[] { + new Object[] { + "0-0", Map.of("0-3", new String[][] {{"field3", "value3"}}) + }, // xautoclaim(streamKey1, groupName1, consumer1, 0L, "0-0") + new Object[] { + "0-0", new String[] {"0-3"} + } // xautoclaimJustId(streamKey1, groupName1, consumer1, 0L, "0-0"); + }); + } + result = + concatenateArrays( + result, + new Object[] { + 1L, // xack(streamKey1, groupName1, new String[] {"0-3"}) + new Object[] {}, // xpending(streamKey1, groupName1, MIN, MAX, 1L) + 0L, // xgroupDelConsumer(streamKey1, groupName1, consumer1) + true, // xgroupDestroy(streamKey1, groupName1) + true, // xgroupDestroy(streamKey1, groupName2) + 1L, // .xdel(streamKey1, new String[] {"0-1", "0-5"}) + "1-0", // xadd(streamKey3, Map.of("f0", "v0"), id("1-0")) + OK, // xgroupCreate(streamKey3, groupName3, "0") + new Map[] {} // xinfoGroups(streamKey3) + }); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + result = + concatenateArrays( + result, + new Object[] { + "1-0", // xadd(streamKey2, Map.of("f0", "v0"), + // StreamAddOptions.builder().id("1-0").build()) + OK, // xgroupCreate(streamKey2, groupName3, "0") + OK, // xgroupSetId(streamKey2, groupName3, "1-0", "0"); + }); + } + + return result; + } + + private static Object[] geospatialCommands(BaseTransaction transaction) { + final String geoKey1 = "{geoKey}-1-" + UUID.randomUUID(); + final String geoKey2 = "{geoKey}-2-" + UUID.randomUUID(); + + transaction + .geoadd( + geoKey1, + Map.of( + "Palermo", + new GeospatialData(13.361389, 38.115556), + "Catania", + new GeospatialData(15.087269, 37.502669))) + .geopos(geoKey1, new String[] {"Palermo", "Catania"}) + .geodist(geoKey1, "Palermo", "Catania") + .geodist(geoKey1, "Palermo", "Catania", GeoUnit.KILOMETERS) + .geohash(geoKey1, new String[] {"Palermo", "Catania", "NonExisting"}); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .geosearch( + geoKey1, + new GeoSearchOrigin.MemberOrigin("Palermo"), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .geosearch( + geoKey1, + new GeoSearchOrigin.CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS)) + .geosearch( + geoKey1, + new GeoSearchOrigin.MemberOrigin("Palermo"), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withhash().withdist().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)) + .geosearch( + geoKey1, + new GeoSearchOrigin.CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchOptions.builder().withhash().withdist().withcoord().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)) + .geosearchstore( + geoKey2, + geoKey1, + new GeoSearchOrigin.MemberOrigin("Palermo"), + new GeoSearchShape(200, GeoUnit.KILOMETERS), + new GeoSearchResultOptions(SortOrder.ASC)) + .geosearchstore( + geoKey2, + geoKey1, + new GeoSearchOrigin.CoordOrigin(new GeospatialData(15, 37)), + new GeoSearchShape(400, 400, GeoUnit.KILOMETERS), + GeoSearchStoreOptions.builder().storedist().build(), + new GeoSearchResultOptions(SortOrder.ASC, 2)); + } + + var expectedResults = + new Object[] { + 2L, // geoadd(geoKey1, Map.of("Palermo", ..., "Catania", ...)) + new Double[][] { + {13.36138933897018433, 38.11555639549629859}, + {15.08726745843887329, 37.50266842333162032}, + }, // geopos(geoKey1, new String[]{"Palermo", "Catania"}) + 166274.1516, // geodist(geoKey1, "Palermo", "Catania") + 166.2742, // geodist(geoKey1, "Palermo", "Catania", GeoUnit.KILOMETERS) + new String[] { + "sqc8b49rny0", "sqdtr74hyu0", null + } // geohash(geoKey1, new String[] {"Palermo", "Catania", "NonExisting"}) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + new String[] { + "Palermo", "Catania" + }, // geosearch(geoKey1, "Palermo", byradius(200, km)) + new String[] { + "Palermo", "Catania" + }, // geosearch(geoKey1, (15,37), bybox(200,200,km)) + new Object[] { + new Object[] { + "Palermo", + new Object[] { + 0.0, 3479099956230698L, new Object[] {13.361389338970184, 38.1155563954963} + } + }, + new Object[] { + "Catania", + new Object[] { + 166.2742, + 3479447370796909L, + new Object[] {15.087267458438873, 37.50266842333162} + } + } + }, // geosearch(geoKey1, "Palermo", BYRADIUS(200, km), ASC, COUNT 2) + new Object[] { + new Object[] { + "Catania", + new Object[] { + 56.4413, + 3479447370796909L, + new Object[] {15.087267458438873, 37.50266842333162} + } + }, + new Object[] { + "Palermo", + new Object[] { + 190.4424, + 3479099956230698L, + new Object[] {13.361389338970184, 38.1155563954963} + } + }, + }, // geosearch(geoKey1, (15,37), BYBOX(400,400,km), ASC, COUNT 2) + 2L, // geosearch(geoKey2, geoKey1, (15,37), BYBOX(400,400,km), ASC, COUNT 2) + 2L, // geosearch(geoKey2, geoKey1, (15,37), BYBOX(400,400,km), STOREDIST, ASC, COUNT + // 2) + }); + } + + return expectedResults; + } + + private static Object[] scriptingAndFunctionsCommands(BaseTransaction transaction) { + if (SERVER_VERSION.isLowerThan("7.0.0")) { + return new Object[0]; + } + + final String libName = "mylib1T"; + final String funcName = "myfunc1T"; + + // function $funcName returns first argument + final String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true); + + var expectedFuncData = + new HashMap() { + { + put("name", funcName); + put("description", null); + put("flags", Set.of("no-writes")); + } + }; + + var expectedLibData = + new Map[] { + Map.of( + "library_name", + libName, + "engine", + "LUA", + "functions", + new Object[] {expectedFuncData}, + "library_code", + code) + }; + + var expectedFunctionStatsNonEmpty = + new HashMap>() { + { + put("running_script", null); + put("engines", Map.of("LUA", Map.of("libraries_count", 1L, "functions_count", 1L))); + } + }; + var expectedFunctionStatsEmpty = + new HashMap>() { + { + put("running_script", null); + put("engines", Map.of("LUA", Map.of("libraries_count", 0L, "functions_count", 0L))); + } + }; + + transaction + .functionFlush(SYNC) + .functionList(false) + .functionLoad(code, false) + .functionLoad(code, true) + .functionStats() + .fcall(funcName, new String[0], new String[] {"a", "b"}) + .fcall(funcName, new String[] {"a", "b"}) + .fcallReadOnly(funcName, new String[0], new String[] {"a", "b"}) + .fcallReadOnly(funcName, new String[] {"a", "b"}) + .functionList("otherLib", false) + .functionList(libName, true) + .functionDelete(libName) + .functionList(true) + .functionStats(); + + return new Object[] { + OK, // functionFlush(SYNC) + new Map[0], // functionList(false) + libName, // functionLoad(code, false) + libName, // functionLoad(code, true) + expectedFunctionStatsNonEmpty, // functionStats() + "a", // fcall(funcName, new String[0], new String[]{"a", "b"}) + "a", // fcall(funcName, new String[] {"a", "b"}) + "a", // fcallReadOnly(funcName, new String[0], new String[]{"a", "b"}) + "a", // fcallReadOnly(funcName, new String[] {"a", "b"}) + new Map[0], // functionList("otherLib", false) + expectedLibData, // functionList(libName, true) + OK, // functionDelete(libName) + new Map[0], // functionList(true) + expectedFunctionStatsEmpty, // functionStats() + }; + } + + private static Object[] bitmapCommands(BaseTransaction transaction) { + String key1 = "{bitmapKey}-1" + UUID.randomUUID(); + String key2 = "{bitmapKey}-2" + UUID.randomUUID(); + String key3 = "{bitmapKey}-3" + UUID.randomUUID(); + String key4 = "{bitmapKey}-4" + UUID.randomUUID(); + BitFieldGet bitFieldGet = new BitFieldGet(new SignedEncoding(5), new Offset(3)); + BitFieldSet bitFieldSet = new BitFieldSet(new UnsignedEncoding(10), new OffsetMultiplier(3), 4); + + transaction + .set(key1, "foobar") + .bitcount(key1) + .bitcount(key1, 1, 1) + .setbit(key2, 1, 1) + .setbit(key2, 1, 0) + .getbit(key1, 1) + .bitpos(key1, 1) + .bitpos(key1, 1, 3) + .bitpos(key1, 1, 3, 5) + .set(key3, "abcdef") + .bitop(BitwiseOperation.AND, key4, new String[] {key1, key3}) + .get(key4) + .bitfieldReadOnly(key1, new BitFieldReadOnlySubCommands[] {bitFieldGet}) + .bitfield(key1, new BitFieldSubCommands[] {bitFieldSet}); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .set(key3, "foobar") + .bitcount(key3, 5, 30, BitmapIndexType.BIT) + .bitpos(key3, 1, 44, 50, BitmapIndexType.BIT); + } + + var expectedResults = + new Object[] { + OK, // set(key1, "foobar") + 26L, // bitcount(key1) + 6L, // bitcount(key1, 1, 1) + 0L, // setbit(key2, 1, 1) + 1L, // setbit(key2, 1, 0) + 1L, // getbit(key1, 1) + 1L, // bitpos(key, 1) + 25L, // bitpos(key, 1, 3) + 25L, // bitpos(key, 1, 3, 5) + OK, // set(key3, "abcdef") + 6L, // bitop(BitwiseOperation.AND, key4, new String[] {key1, key3}) + "`bc`ab", // get(key4) + new Long[] {6L}, // bitfieldReadOnly(key1, BitFieldReadOnlySubCommands) + new Long[] {609L}, // bitfield(key1, BitFieldSubCommands) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + return concatenateArrays( + expectedResults, + new Object[] { + OK, // set(key1, "foobar") + 17L, // bitcount(key, 5, 30, BitmapIndexType.BIT) + 46L, // bitpos(key, 1, 44, 50, BitmapIndexType.BIT) + }); + } + return expectedResults; + } + + private static Object[] pubsubCommands(BaseTransaction transaction) { + transaction.publish("message", "Tchannel"); + + return new Object[] { + 0L, // publish("message", "Tchannel") + }; + } } diff --git a/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java b/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java index c3eb503eaf..288b31f5da 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java @@ -1,7 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.cluster; -import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestConfiguration.SERVER_VERSION; import static glide.TestUtilities.commonClusterClientConfig; import static glide.TestUtilities.getRandomString; import static glide.api.BaseClient.OK; @@ -10,8 +10,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import glide.api.RedisClusterClient; -import glide.api.models.configuration.RedisCredentials; +import glide.api.GlideClusterClient; +import glide.api.models.configuration.ServerCredentials; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.RequestException; import java.util.concurrent.ExecutionException; @@ -27,11 +27,11 @@ public class ClusterClientTests { public void register_client_name_and_version() { String minVersion = "7.2.0"; assumeTrue( - REDIS_VERSION.isGreaterThanOrEqualTo(minVersion), - "Redis version required >= " + minVersion); + SERVER_VERSION.isGreaterThanOrEqualTo(minVersion), + "Valkey version required >= " + minVersion); - RedisClusterClient client = - RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get(); + GlideClusterClient client = + GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); String info = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get().getSingleValue(); @@ -44,8 +44,8 @@ public void register_client_name_and_version() { @SneakyThrows @Test public void can_connect_with_auth_requirepass() { - RedisClusterClient client = - RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get(); + GlideClusterClient client = + GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); String password = "TEST_AUTH"; client.customCommand(new String[] {"CONFIG", "SET", "requirepass", password}).get(); @@ -54,14 +54,14 @@ public void can_connect_with_auth_requirepass() { ExecutionException exception = assertThrows( ExecutionException.class, - () -> RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get()); + () -> GlideClusterClient.createClient(commonClusterClientConfig().build()).get()); assertTrue(exception.getCause() instanceof ClosingException); // Creation of a new client with credentials - RedisClusterClient auth_client = - RedisClusterClient.CreateClient( + GlideClusterClient auth_client = + GlideClusterClient.createClient( commonClusterClientConfig() - .credentials(RedisCredentials.builder().password(password).build()) + .credentials(ServerCredentials.builder().password(password).build()) .build()) .get(); @@ -81,8 +81,8 @@ public void can_connect_with_auth_requirepass() { @SneakyThrows @Test public void can_connect_with_auth_acl() { - RedisClusterClient client = - RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get(); + GlideClusterClient client = + GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); String username = "testuser"; String password = "TEST_AUTH"; @@ -112,11 +112,11 @@ public void can_connect_with_auth_acl() { assertEquals(OK, client.set(key, value).get()); // Creation of a new cluster client with credentials - RedisClusterClient testUserClient = - RedisClusterClient.CreateClient( + GlideClusterClient testUserClient = + GlideClusterClient.createClient( commonClusterClientConfig() .credentials( - RedisCredentials.builder().username(username).password(password).build()) + ServerCredentials.builder().username(username).password(password).build()) .build()) .get(); @@ -135,8 +135,8 @@ public void can_connect_with_auth_acl() { @SneakyThrows @Test public void client_name() { - RedisClusterClient client = - RedisClusterClient.CreateClient( + GlideClusterClient client = + GlideClusterClient.createClient( commonClusterClientConfig().clientName("TEST_CLIENT_NAME").build()) .get(); @@ -150,8 +150,8 @@ public void client_name() { @Test @SneakyThrows public void closed_client_throws_ExecutionException_with_ClosingException_as_cause() { - RedisClusterClient client = - RedisClusterClient.CreateClient(commonClusterClientConfig().build()).get(); + GlideClusterClient client = + GlideClusterClient.createClient(commonClusterClientConfig().build()).get(); client.close(); ExecutionException executionException = diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index 498893466f..f2b6462d1d 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -1,39 +1,58 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.cluster; -import static glide.TransactionTestUtilities.transactionTest; -import static glide.TransactionTestUtilities.transactionTestResult; +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.assertDeepEquals; +import static glide.TestUtilities.generateLuaLibCode; import static glide.api.BaseClient.OK; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import glide.TestConfiguration; -import glide.api.RedisClusterClient; +import glide.TransactionTestUtilities.TransactionBuilder; +import glide.api.GlideClusterClient; import glide.api.models.ClusterTransaction; +import glide.api.models.GlideString; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.configuration.GlideClusterClientConfiguration; import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClusterClientConfiguration; +import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; +import glide.api.models.configuration.RequestRoutingConfiguration.SlotIdRoute; +import glide.api.models.configuration.RequestRoutingConfiguration.SlotType; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; @Timeout(10) // seconds public class ClusterTransactionTests { - private static RedisClusterClient clusterClient = null; + private static GlideClusterClient clusterClient = null; @BeforeAll @SneakyThrows public static void init() { clusterClient = - RedisClusterClient.CreateClient( - RedisClusterClientConfiguration.builder() + GlideClusterClient.createClient( + GlideClusterClientConfiguration.builder() .address(NodeAddress.builder().port(TestConfiguration.CLUSTER_PORTS[0]).build()) .requestTimeout(5000) .build()) @@ -54,17 +73,6 @@ public void custom_command_info() { assertTrue(((String) result[0]).contains("# Stats")); } - @Test - @SneakyThrows - public void WATCH_transaction_failure_returns_null() { - ClusterTransaction transaction = new ClusterTransaction(); - transaction.get("key"); - assertEquals( - OK, clusterClient.customCommand(new String[] {"WATCH", "key"}).get().getSingleValue()); - assertEquals(OK, clusterClient.set("key", "foo").get()); - assertNull(clusterClient.exec(transaction).get()); - } - @Test @SneakyThrows public void info_simple_route_test() { @@ -75,14 +83,49 @@ public void info_simple_route_test() { assertTrue(((String) result[1]).contains("# Stats")); } + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("glide.TransactionTestUtilities#getCommonTransactionBuilders") + public void transactions_with_group_of_commands(String testName, TransactionBuilder builder) { + ClusterTransaction transaction = new ClusterTransaction(); + Object[] expectedResult = builder.apply(transaction); + + Object[] results = clusterClient.exec(transaction).get(); + assertDeepEquals(expectedResult, results); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("glide.TransactionTestUtilities#getPrimaryNodeTransactionBuilders") + public void keyless_transactions_with_group_of_commands( + String testName, TransactionBuilder builder) { + ClusterTransaction transaction = new ClusterTransaction(); + Object[] expectedResult = builder.apply(transaction); + + SingleNodeRoute route = new SlotIdRoute(1, SlotType.PRIMARY); + Object[] results = clusterClient.exec(transaction, route).get(); + assertDeepEquals(expectedResult, results); + } + @SneakyThrows @Test - public void test_cluster_transactions() { - ClusterTransaction transaction = (ClusterTransaction) transactionTest(new ClusterTransaction()); - Object[] expectedResult = transactionTestResult(); + public void test_transaction_large_values() { + int length = 1 << 25; // 33mb + String key = "0".repeat(length); + String value = "0".repeat(length); + + ClusterTransaction transaction = new ClusterTransaction(); + transaction.set(key, value); + transaction.get(key); + + Object[] expectedResult = + new Object[] { + OK, // transaction.set(key, value); + value, // transaction.get(key); + }; - Object[] results = clusterClient.exec(transaction, RANDOM).get(); - assertArrayEquals(expectedResult, results); + Object[] result = clusterClient.exec(transaction).get(); + assertArrayEquals(expectedResult, result); } @Test @@ -92,4 +135,312 @@ public void lastsave() { var response = clusterClient.exec(new ClusterTransaction().lastsave()).get(); assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); } + + @Test + @SneakyThrows + public void objectFreq() { + String objectFreqKey = "key"; + String maxmemoryPolicy = "maxmemory-policy"; + String oldPolicy = + clusterClient.configGet(new String[] {maxmemoryPolicy}).get().get(maxmemoryPolicy); + try { + ClusterTransaction transaction = new ClusterTransaction(); + transaction.configSet(Map.of(maxmemoryPolicy, "allkeys-lfu")); + transaction.set(objectFreqKey, ""); + transaction.objectFreq(objectFreqKey); + var response = clusterClient.exec(transaction).get(); + assertEquals(OK, response[0]); + assertEquals(OK, response[1]); + assertTrue((long) response[2] >= 0L); + } finally { + clusterClient.configSet(Map.of(maxmemoryPolicy, oldPolicy)); + } + } + + @Test + @SneakyThrows + public void objectIdletime() { + String objectIdletimeKey = "key"; + ClusterTransaction transaction = new ClusterTransaction(); + transaction.set(objectIdletimeKey, ""); + transaction.objectIdletime(objectIdletimeKey); + var response = clusterClient.exec(transaction).get(); + assertEquals(OK, response[0]); + assertTrue((long) response[1] >= 0L); + } + + @Test + @SneakyThrows + public void objectRefcount() { + String objectRefcountKey = "key"; + ClusterTransaction transaction = new ClusterTransaction(); + transaction.set(objectRefcountKey, ""); + transaction.objectRefcount(objectRefcountKey); + var response = clusterClient.exec(transaction).get(); + assertEquals(OK, response[0]); + assertTrue((long) response[1] >= 0L); + } + + @Test + @SneakyThrows + public void zrank_zrevrank_withscores() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")); + String zSetKey1 = "{key}:zsetKey1-" + UUID.randomUUID(); + ClusterTransaction transaction = new ClusterTransaction(); + transaction.zadd(zSetKey1, Map.of("one", 1.0, "two", 2.0, "three", 3.0)); + transaction.zrankWithScore(zSetKey1, "one"); + transaction.zrevrankWithScore(zSetKey1, "one"); + + Object[] result = clusterClient.exec(transaction).get(); + assertEquals(3L, result[0]); + assertArrayEquals(new Object[] {0L, 1.0}, (Object[]) result[1]); + assertArrayEquals(new Object[] {2L, 1.0}, (Object[]) result[2]); + } + + @Test + @SneakyThrows + public void watch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String key4 = "{key}-4" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2, key3}; + ClusterTransaction setFoobarTransaction = new ClusterTransaction(); + ClusterTransaction setHelloTransaction = new ClusterTransaction(); + String[] expectedExecResponse = new String[] {OK, OK, OK}; + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + assertEquals(OK, clusterClient.watch(keys).get()); + assertEquals(OK, clusterClient.set(key2, helloString).get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString).set(key3, foobarString); + assertNull(clusterClient.exec(setFoobarTransaction).get()); // Sanity check + assertNull(clusterClient.get(key1).get()); + assertEquals(helloString, clusterClient.get(key2).get()); + assertNull(clusterClient.get(key3).get()); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + assertEquals(OK, clusterClient.watch(keys).get()); + assertEquals(helloString, clusterClient.get(key2).get()); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(foobarString, clusterClient.get(key1).get()); // Sanity check + assertEquals(foobarString, clusterClient.get(key2).get()); + assertEquals(foobarString, clusterClient.get(key3).get()); + + // Transaction executes command successfully with unmodified watched keys + assertEquals(OK, clusterClient.watch(keys).get()); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(foobarString, clusterClient.get(key1).get()); // Sanity check + assertEquals(foobarString, clusterClient.get(key2).get()); + assertEquals(foobarString, clusterClient.get(key3).get()); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + assertEquals(OK, clusterClient.watch(new String[] {key4}).get()); + setHelloTransaction.set(key1, helloString).set(key2, helloString).set(key3, helloString); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setHelloTransaction).get()); + assertEquals(helloString, clusterClient.get(key1).get()); // Sanity check + assertEquals(helloString, clusterClient.get(key2).get()); + assertEquals(helloString, clusterClient.get(key3).get()); + + // WATCH can not have an empty String array parameter + // Test fails due to https://github.com/amazon-contributing/redis-rs/issues/158 + // ExecutionException executionException = + // assertThrows(ExecutionException.class, () -> clusterClient.watch(new String[] + // {}).get()); + // assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @Test + @SneakyThrows + public void unwatch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2}; + ClusterTransaction setFoobarTransaction = new ClusterTransaction(); + String[] expectedExecResponse = new String[] {OK, OK}; + + // UNWATCH returns OK when there no watched keys + assertEquals(OK, clusterClient.unwatch().get()); + + // Transaction executes successfully after modifying a watched key then calling UNWATCH + assertEquals(OK, clusterClient.watch(keys).get()); + assertEquals(OK, clusterClient.set(key2, helloString).get()); + assertEquals(OK, clusterClient.unwatch().get()); + assertEquals(OK, clusterClient.unwatch(ALL_PRIMARIES).get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString); + assertArrayEquals(expectedExecResponse, clusterClient.exec(setFoobarTransaction).get()); + assertEquals(foobarString, clusterClient.get(key1).get()); + assertEquals(foobarString, clusterClient.get(key2).get()); + } + + @Test + @SneakyThrows + public void spublish() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + ClusterTransaction transaction = new ClusterTransaction().publish("messagae", "Schannel", true); + + assertArrayEquals(new Object[] {0L}, clusterClient.exec(transaction).get()); + } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String[] descendingList = new String[] {"3", "2", "1"}; + ClusterTransaction transaction = new ClusterTransaction(); + transaction + .lpush(key1, new String[] {"3", "1", "2"}) + .sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .sortStore(key1, key2, SortClusterOptions.builder().orderBy(DESC).build()) + .lrange(key2, 0, -1); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction.sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()); + } + + Object[] results = clusterClient.exec(transaction).get(); + Object[] expectedResult = + new Object[] { + 3L, // lpush(key1, new String[] {"3", "1", "2"}) + descendingList, // sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + 3L, // sortStore(key1, key2, DESC)) + descendingList, // lrange(key2, 0, -1) + }; + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResult = + concatenateArrays( + expectedResult, new Object[] {descendingList} // sortReadOnly(key1, DESC) + ); + } + + assertDeepEquals(expectedResult, results); + } + + @SneakyThrows + @Test + public void waitTest() { + // setup + String key = UUID.randomUUID().toString(); + long numreplicas = 1L; + long timeout = 1000L; + ClusterTransaction transaction = new ClusterTransaction(); + + transaction.set(key, "value").wait(numreplicas, timeout); + Object[] results = clusterClient.exec(transaction).get(); + Object[] expectedResult = + new Object[] { + OK, // set(key, "value") + 0L, // wait(numreplicas, timeout) + }; + assertEquals(expectedResult[0], results[0]); + assertTrue((Long) expectedResult[1] <= (Long) results[1]); + } + + @Test + @SneakyThrows + public void test_transaction_function_dump_restore() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")); + String libName = "mylib"; + String funcName = "myfun"; + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true); + + // Setup + clusterClient.functionLoad(code, true).get(); + + // Verify functionDump + ClusterTransaction transaction = new ClusterTransaction().withBinaryOutput().functionDump(); + Object[] result = clusterClient.exec(transaction).get(); + GlideString payload = (GlideString) (result[0]); + + // Verify functionRestore + transaction = new ClusterTransaction(); + transaction.functionRestore(payload.getBytes(), FunctionRestorePolicy.REPLACE); + // For the cluster mode, PRIMARY SlotType is required to avoid the error: + // "RequestError: An error was signalled by the server - + // ReadOnly: You can't write against a read only replica." + Object[] response = clusterClient.exec(transaction, new SlotIdRoute(1, SlotType.PRIMARY)).get(); + assertEquals(OK, response[0]); + } + + @Test + @SneakyThrows + public void test_transaction_xinfoStream() { + ClusterTransaction transaction = new ClusterTransaction(); + final String streamKey = "{streamKey}-" + UUID.randomUUID(); + LinkedHashMap expectedStreamInfo = + new LinkedHashMap<>() { + { + put("radix-tree-keys", 1L); + put("radix-tree-nodes", 2L); + put("length", 1L); + put("groups", 0L); + put("first-entry", new Object[] {"0-1", new Object[] {"field1", "value1"}}); + put("last-generated-id", "0-1"); + put("last-entry", new Object[] {"0-1", new Object[] {"field1", "value1"}}); + } + }; + LinkedHashMap expectedStreamFullInfo = + new LinkedHashMap<>() { + { + put("radix-tree-keys", 1L); + put("radix-tree-nodes", 2L); + put("entries", new Object[][] {{"0-1", new Object[] {"field1", "value1"}}}); + put("length", 1L); + put("groups", new Object[0]); + put("last-generated-id", "0-1"); + } + }; + + transaction + .xadd(streamKey, Map.of("field1", "value1"), StreamAddOptions.builder().id("0-1").build()) + .xinfoStream(streamKey) + .xinfoStreamFull(streamKey); + + Object[] results = clusterClient.exec(transaction).get(); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedStreamInfo.put("max-deleted-entry-id", "0-0"); + expectedStreamInfo.put("entries-added", 1L); + expectedStreamInfo.put("recorded-first-entry-id", "0-1"); + expectedStreamFullInfo.put("max-deleted-entry-id", "0-0"); + expectedStreamFullInfo.put("entries-added", 1L); + expectedStreamFullInfo.put("recorded-first-entry-id", "0-1"); + } + + assertDeepEquals( + new Object[] { + "0-1", // xadd(streamKey1, Map.of("field1", "value1"), ... .id("0-1").build()); + expectedStreamInfo, // xinfoStream(streamKey) + expectedStreamFullInfo, // xinfoStreamFull(streamKey1) + }, + results); + } + + @SneakyThrows + @Test + public void binary_strings() { + String key = UUID.randomUUID().toString(); + clusterClient.set(key, "_").get(); + // use dump to ensure that we have non-string convertible bytes + var bytes = clusterClient.dump(gs(key)).get(); + + var transaction = + new ClusterTransaction().withBinaryOutput().set(gs(key), gs(bytes)).get(gs(key)); + + var responses = clusterClient.exec(transaction).get(); + + assertDeepEquals( + new Object[] { + OK, gs(bytes), + }, + responses); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 81dd2586be..89ca922122 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -1,11 +1,24 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.cluster; -import static glide.TestConfiguration.CLUSTER_PORTS; -import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.assertDeepEquals; +import static glide.TestUtilities.checkFunctionListResponse; +import static glide.TestUtilities.checkFunctionListResponseBinary; +import static glide.TestUtilities.checkFunctionStatsBinaryResponse; +import static glide.TestUtilities.checkFunctionStatsResponse; +import static glide.TestUtilities.commonClusterClientConfig; +import static glide.TestUtilities.createLuaLibWithLongRunningFunction; +import static glide.TestUtilities.generateLuaLibCode; +import static glide.TestUtilities.generateLuaLibCodeBinary; import static glide.TestUtilities.getFirstEntryFromMultiValue; import static glide.TestUtilities.getValueFromInfo; +import static glide.TestUtilities.parseInfoResponseToMap; +import static glide.TestUtilities.waitForNotBusy; import static glide.api.BaseClient.OK; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.InfoOptions.Section.CLIENTS; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.COMMANDSTATS; @@ -13,44 +26,83 @@ import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.REPLICATION; +import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; -import static glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; +import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.function.FunctionRestorePolicy.APPEND; +import static glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; +import static glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; import static glide.api.models.configuration.RequestRoutingConfiguration.SlotType.PRIMARY; +import static glide.api.models.configuration.RequestRoutingConfiguration.SlotType.REPLICA; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import glide.api.RedisClusterClient; +import glide.api.GlideClusterClient; +import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClusterClientConfiguration; +import glide.api.models.commands.ListDirection; +import glide.api.models.commands.RangeOptions.RangeByIndex; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.commands.geospatial.GeoSearchOrigin; +import glide.api.models.commands.geospatial.GeoSearchResultOptions; +import glide.api.models.commands.geospatial.GeoSearchShape; +import glide.api.models.commands.geospatial.GeoSearchStoreOptions; +import glide.api.models.commands.geospatial.GeoUnit; +import glide.api.models.commands.scan.ClusterScanCursor; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; -import glide.api.models.exceptions.RedisException; +import glide.api.models.exceptions.GlideException; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; @Timeout(10) // seconds public class CommandTests { - private static RedisClusterClient clusterClient = null; + private static GlideClusterClient clusterClient = null; private static final String INITIAL_VALUE = "VALUE"; @@ -68,8 +120,8 @@ public class CommandTests { "Cluster", "Keyspace"); public static final List EVERYTHING_INFO_SECTIONS = - REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0") - // Latencystats was added in redis 7 + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0") + // Latencystats was added in Valkey 7 ? List.of( "Server", "Clients", @@ -102,11 +154,7 @@ public class CommandTests { @SneakyThrows public static void init() { clusterClient = - RedisClusterClient.CreateClient( - RedisClusterClientConfiguration.builder() - .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build()) - .requestTimeout(5000) - .build()) + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(7000).build()) .get(); } @@ -158,6 +206,13 @@ public void ping_with_message() { assertEquals("H3LL0", data); } + @Test + @SneakyThrows + public void ping_binary_with_message() { + GlideString data = clusterClient.ping(gs("H3LL0")).get(); + assertEquals(gs("H3LL0"), data); + } + @Test @SneakyThrows public void ping_with_route() { @@ -172,6 +227,13 @@ public void ping_with_message_with_route() { assertEquals("H3LL0", data); } + @Test + @SneakyThrows + public void ping_binary_with_message_with_route() { + GlideString data = clusterClient.ping(gs("H3LL0"), ALL_PRIMARIES).get(); + assertEquals(gs("H3LL0"), data); + } + @Test @SneakyThrows public void info_without_options() { @@ -211,7 +273,7 @@ public void info_with_multi_node_route() { @SneakyThrows public void info_with_multiple_options() { InfoOptions.InfoOptionsBuilder builder = InfoOptions.builder().section(CLUSTER); - if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { builder.section(CPU).section(MEMORY); } InfoOptions options = builder.build(); @@ -256,7 +318,7 @@ public void info_with_single_node_route_and_options() { (String) ((Object[]) ((Object[]) ((Object[]) slotData.getSingleValue())[0])[2])[2]; InfoOptions.InfoOptionsBuilder builder = InfoOptions.builder().section(CLIENTS); - if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { builder.section(COMMANDSTATS).section(REPLICATION); } InfoOptions options = builder.build(); @@ -274,7 +336,7 @@ public void info_with_single_node_route_and_options() { @SneakyThrows public void info_with_multi_node_route_and_options() { InfoOptions.InfoOptionsBuilder builder = InfoOptions.builder().section(CLIENTS); - if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { builder.section(COMMANDSTATS).section(REPLICATION); } InfoOptions options = builder.build(); @@ -368,10 +430,16 @@ public void config_reset_stat() { @Test @SneakyThrows public void config_rewrite_non_existent_config_file() { - // The setup for the Integration Tests server does not include a configuration file for Redis. - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> clusterClient.configRewrite().get()); - assertTrue(executionException.getCause() instanceof RequestException); + var info = clusterClient.info(InfoOptions.builder().section(SERVER).build(), RANDOM).get(); + var configFile = parseInfoResponseToMap(info.getSingleValue()).get("config_file"); + + if (configFile.isEmpty()) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } else { + assertEquals(OK, clusterClient.configRewrite().get()); + } } // returns the line that contains the word "myself", up to that point. This is done because the @@ -390,7 +458,7 @@ public void configGet_with_no_args_returns_error() { var exception = assertThrows( ExecutionException.class, () -> clusterClient.configGet(new String[] {}).get()); - assertTrue(exception.getCause() instanceof RedisException); + assertTrue(exception.getCause() instanceof GlideException); } @Test @@ -405,7 +473,7 @@ public void configGet_with_wildcard() { @Test @SneakyThrows public void configGet_with_multiple_params() { - assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); var data = clusterClient.configGet(new String[] {"pidfile", "logfile"}).get(); assertAll( () -> assertEquals(2, data.size()), @@ -524,6 +592,26 @@ public void echo_with_route() { multiPayload.forEach((key, value) -> assertEquals(message, value)); } + @SneakyThrows + @Test + public void echo_gs() { + byte[] message = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + GlideString response = clusterClient.echo(gs(message)).get(); + assertEquals(gs(message), response); + } + + @SneakyThrows + @Test + public void echo_gs_with_route() { + byte[] message = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + GlideString singlePayload = clusterClient.echo(gs(message), RANDOM).get().getSingleValue(); + assertEquals(gs(message), singlePayload); + + Map multiPayload = + clusterClient.echo(gs(message), ALL_NODES).get().getMultiValue(); + multiPayload.forEach((key, value) -> assertEquals(gs(message), value)); + } + @Test @SneakyThrows public void time() { @@ -571,4 +659,2318 @@ public void lastsave() { assertTrue(Instant.ofEpochSecond(value).isAfter(yesterday)); } } + + @Test + @SneakyThrows + public void lolwut_lolwut() { + var response = clusterClient.lolwut().get(); + System.out.printf("%nLOLWUT cluster client standard response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + response = clusterClient.lolwut(new int[] {50, 20}).get(); + System.out.printf( + "%nLOLWUT cluster client standard response with params 50 20%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + response = clusterClient.lolwut(6).get(); + System.out.printf("%nLOLWUT cluster client ver 6 response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + response = clusterClient.lolwut(5, new int[] {30, 4, 4}).get(); + System.out.printf("%nLOLWUT cluster client ver 5 response with params 30 4 4%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + var clusterResponse = clusterClient.lolwut(ALL_NODES).get(); + for (var nodeResponse : clusterResponse.getMultiValue().values()) { + assertTrue(nodeResponse.contains("Redis ver. " + SERVER_VERSION)); + } + + clusterResponse = clusterClient.lolwut(new int[] {10, 20}, ALL_NODES).get(); + for (var nodeResponse : clusterResponse.getMultiValue().values()) { + assertTrue(nodeResponse.contains("Redis ver. " + SERVER_VERSION)); + } + + clusterResponse = clusterClient.lolwut(2, RANDOM).get(); + assertTrue(clusterResponse.getSingleValue().contains("Redis ver. " + SERVER_VERSION)); + + clusterResponse = clusterClient.lolwut(2, new int[] {10, 20}, RANDOM).get(); + assertTrue(clusterResponse.getSingleValue().contains("Redis ver. " + SERVER_VERSION)); + } + + @Test + @SneakyThrows + public void dbsize_and_flushdb() { + boolean is62orHigher = SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"); + + assertEquals(OK, clusterClient.flushall().get()); + // dbsize should be 0 after flushall() because all keys have been deleted + assertEquals(0L, clusterClient.dbsize().get()); + + int numKeys = 10; + for (int i = 0; i < numKeys; i++) { + assertEquals(OK, clusterClient.set(UUID.randomUUID().toString(), "foo").get()); + } + assertEquals(10L, clusterClient.dbsize(ALL_PRIMARIES).get()); + + // test dbsize with routing - flush the database first to ensure the set() call is directed to a + // node with 0 keys. + assertEquals(OK, clusterClient.flushdb().get()); + assertEquals(0L, clusterClient.dbsize().get()); + + String key = UUID.randomUUID().toString(); + SingleNodeRoute route = new SlotKeyRoute(key, PRIMARY); + + // add a key, measure DB size, flush DB and measure again - with all arg combinations + assertEquals(OK, clusterClient.set(key, "foo").get()); + assertEquals(1L, clusterClient.dbsize(route).get()); + if (is62orHigher) { + assertEquals(OK, clusterClient.flushdb(SYNC).get()); + } else { + assertEquals(OK, clusterClient.flushdb(ASYNC).get()); + } + assertEquals(0L, clusterClient.dbsize().get()); + + assertEquals(OK, clusterClient.set(key, "foo").get()); + assertEquals(1L, clusterClient.dbsize(route).get()); + assertEquals(OK, clusterClient.flushdb(route).get()); + assertEquals(0L, clusterClient.dbsize(route).get()); + + assertEquals(OK, clusterClient.set(key, "foo").get()); + assertEquals(1L, clusterClient.dbsize(route).get()); + if (is62orHigher) { + assertEquals(OK, clusterClient.flushdb(SYNC, route).get()); + } else { + assertEquals(OK, clusterClient.flushdb(ASYNC, route).get()); + } + assertEquals(0L, clusterClient.dbsize(route).get()); + + if (!is62orHigher) { + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.flushdb(SYNC).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + } + + @Test + @SneakyThrows + public void objectFreq() { + String key = UUID.randomUUID().toString(); + String maxmemoryPolicy = "maxmemory-policy"; + String oldPolicy = + clusterClient.configGet(new String[] {maxmemoryPolicy}).get().get(maxmemoryPolicy); + try { + assertEquals(OK, clusterClient.configSet(Map.of(maxmemoryPolicy, "allkeys-lfu")).get()); + assertEquals(OK, clusterClient.set(key, "").get()); + assertTrue(clusterClient.objectFreq(key).get() >= 0L); + } finally { + clusterClient.configSet(Map.of(maxmemoryPolicy, oldPolicy)).get(); + } + } + + public static Stream callCrossSlotCommandsWhichShouldFail() { + return Stream.of( + Arguments.of("smove", null, clusterClient.smove("abc", "zxy", "lkn")), + Arguments.of("rename", null, clusterClient.rename("abc", "xyz")), + Arguments.of("renamenx", null, clusterClient.renamenx("abc", "zxy")), + Arguments.of( + "sinterstore", null, clusterClient.sinterstore("abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "sinterstore_gs", + null, + clusterClient.sinterstore(gs("abc"), new GlideString[] {gs("zxy"), gs("lkn")})), + Arguments.of("sdiff", null, clusterClient.sdiff(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "sdiff_gs", + null, + clusterClient.sdiff(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")})), + Arguments.of( + "sdiffstore", null, clusterClient.sdiffstore("abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "sdiffstore_gs", + null, + clusterClient.sdiffstore(gs("abc"), new GlideString[] {gs("zxy"), gs("lkn")})), + Arguments.of("sinter", null, clusterClient.sinter(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "sinter_gs", + null, + clusterClient.sinter(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")})), + Arguments.of( + "sunionstore", null, clusterClient.sunionstore("abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "sunionstore binary", + null, + clusterClient.sunionstore(gs("abc"), new GlideString[] {gs("zxy"), gs("lkn")})), + Arguments.of("zdiff", null, clusterClient.zdiff(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "zdiffWithScores", + null, + clusterClient.zdiffWithScores(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "zdiffstore", null, clusterClient.zdiffstore("abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "zunion", null, clusterClient.zunion(new KeyArray(new String[] {"abc", "zxy", "lkn"}))), + Arguments.of( + "zinter", + "6.2.0", + clusterClient.zinter(new KeyArray(new String[] {"abc", "zxy", "lkn"}))), + Arguments.of( + "zrangestore", null, clusterClient.zrangestore("abc", "zxy", new RangeByIndex(3, 1))), + Arguments.of( + "zinterstore", + null, + clusterClient.zinterstore("foo", new KeyArray(new String[] {"abc", "zxy", "lkn"}))), + Arguments.of( + "zintercard", "7.0.0", clusterClient.zintercard(new String[] {"abc", "zxy", "lkn"})), + Arguments.of("brpop", null, clusterClient.brpop(new String[] {"abc", "zxy", "lkn"}, .1)), + Arguments.of( + "brpop binary", + null, + clusterClient.brpop(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, .1)), + Arguments.of("blpop", null, clusterClient.blpop(new String[] {"abc", "zxy", "lkn"}, .1)), + Arguments.of( + "blpop binary", + null, + clusterClient.blpop(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, .1)), + Arguments.of("pfcount", null, clusterClient.pfcount(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "pfcount binary", + null, + clusterClient.pfcount(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")})), + Arguments.of("pfmerge", null, clusterClient.pfmerge("abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "pfmerge binary", + null, + clusterClient.pfmerge(gs("abc"), new GlideString[] {gs("zxy"), gs("lkn")})), + Arguments.of( + "bzpopmax", "5.0.0", clusterClient.bzpopmax(new String[] {"abc", "zxy", "lkn"}, .1)), + Arguments.of( + "bzpopmax binary", + "5.0.0", + clusterClient.bzpopmax(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, .1)), + Arguments.of( + "bzpopmin", "5.0.0", clusterClient.bzpopmin(new String[] {"abc", "zxy", "lkn"}, .1)), + Arguments.of( + "bzpopmin binary", + "5.0.0", + clusterClient.bzpopmin(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, .1)), + Arguments.of( + "zmpop", "7.0.0", clusterClient.zmpop(new String[] {"abc", "zxy", "lkn"}, MAX)), + Arguments.of( + "zmpop binary", + "7.0.0", + clusterClient.zmpop(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, MAX)), + Arguments.of( + "bzmpop", "7.0.0", clusterClient.bzmpop(new String[] {"abc", "zxy", "lkn"}, MAX, .1)), + Arguments.of( + "bzmpop binary", + "7.0.0", + clusterClient.bzmpop(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, MAX, .1)), + Arguments.of( + "lmpop", + "7.0.0", + clusterClient.lmpop(new String[] {"abc", "def"}, ListDirection.LEFT, 1L)), + Arguments.of( + "lmpop binary", + "7.0.0", + clusterClient.lmpop(new GlideString[] {gs("abc"), gs("def")}, ListDirection.LEFT, 1L)), + Arguments.of( + "bitop", + null, + clusterClient.bitop(BitwiseOperation.OR, "abc", new String[] {"zxy", "lkn"})), + Arguments.of( + "blmpop", + "7.0.0", + clusterClient.blmpop(new String[] {"abc", "def"}, ListDirection.LEFT, 1L, 0.1)), + Arguments.of( + "blmpop binary", + "7.0.0", + clusterClient.blmpop( + new GlideString[] {gs("abc"), gs("def")}, ListDirection.LEFT, 1L, 0.1)), + Arguments.of( + "lmove", + "6.2.0", + clusterClient.lmove("abc", "def", ListDirection.LEFT, ListDirection.LEFT)), + Arguments.of( + "blmove", + "6.2.0", + clusterClient.blmove("abc", "def", ListDirection.LEFT, ListDirection.LEFT, 1)), + Arguments.of("sintercard", "7.0.0", clusterClient.sintercard(new String[] {"abc", "def"})), + Arguments.of( + "sintercard_gs", + "7.0.0", + clusterClient.sintercard(new GlideString[] {gs("abc"), gs("def")})), + Arguments.of( + "sintercard", "7.0.0", clusterClient.sintercard(new String[] {"abc", "def"}, 1)), + Arguments.of( + "sintercard_gs", + "7.0.0", + clusterClient.sintercard(new GlideString[] {gs("abc"), gs("def")}, 1)), + Arguments.of( + "fcall", + "7.0.0", + clusterClient.fcall("func", new String[] {"abc", "zxy", "lkn"}, new String[0])), + Arguments.of( + "fcall binary", + "7.0.0", + clusterClient.fcall( + gs("func"), + new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, + new GlideString[0])), + Arguments.of( + "fcallReadOnly", + "7.0.0", + clusterClient.fcallReadOnly("func", new String[] {"abc", "zxy", "lkn"}, new String[0])), + Arguments.of( + "fcallReadOnly binary", + "7.0.0", + clusterClient.fcallReadOnly( + gs("func"), + new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")}, + new GlideString[0])), + Arguments.of( + "xread", null, clusterClient.xread(Map.of("abc", "stream1", "zxy", "stream2"))), + Arguments.of("copy", "6.2.0", clusterClient.copy("abc", "def", true)), + Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl"))), + Arguments.of("lcs", "7.0.0", clusterClient.lcs("abc", "def")), + Arguments.of("lcsLEN", "7.0.0", clusterClient.lcsLen("abc", "def")), + Arguments.of("lcsIdx", "7.0.0", clusterClient.lcsIdx("abc", "def")), + Arguments.of("lcsIdx", "7.0.0", clusterClient.lcsIdx("abc", "def", 10)), + Arguments.of("lcsIdxWithMatchLen", "7.0.0", clusterClient.lcsIdxWithMatchLen("abc", "def")), + Arguments.of( + "lcsIdxWithMatchLen", "7.0.0", clusterClient.lcsIdxWithMatchLen("abc", "def", 10)), + Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"})), + Arguments.of( + "sunion binary", + "1.0.0", + clusterClient.sunion(new GlideString[] {gs("abc"), gs("def"), gs("ghi")})), + Arguments.of("sortStore", "1.0.0", clusterClient.sortStore("abc", "def")), + Arguments.of( + "sortStore", + "1.0.0", + clusterClient.sortStore("abc", "def", SortClusterOptions.builder().alpha().build())), + Arguments.of( + "geosearchstore", + "6.2.0", + clusterClient.geosearchstore( + "dest", + "source", + new GeoSearchOrigin.MemberOrigin("abc"), + new GeoSearchShape(1, GeoUnit.METERS), + GeoSearchStoreOptions.builder().build(), + new GeoSearchResultOptions(1, true)))); + } + + @SneakyThrows + @ParameterizedTest(name = "{0} cross slot keys will throw RequestException") + @MethodSource("callCrossSlotCommandsWhichShouldFail") + public void check_throws_cross_slot_error( + String testName, String minVer, CompletableFuture future) { + if (minVer != null) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo(minVer)); + } + var executionException = assertThrows(ExecutionException.class, future::get); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); + } + + public static Stream callCrossSlotCommandsWhichShouldPass() { + return Stream.of( + Arguments.of("exists", clusterClient.exists(new String[] {"abc", "zxy", "lkn"})), + Arguments.of("unlink", clusterClient.unlink(new String[] {"abc", "zxy", "lkn"})), + Arguments.of("del", clusterClient.del(new String[] {"abc", "zxy", "lkn"})), + Arguments.of("mget", clusterClient.mget(new String[] {"abc", "zxy", "lkn"})), + Arguments.of("mset", clusterClient.mset(Map.of("abc", "1", "zxy", "2", "lkn", "3"))), + Arguments.of("touch", clusterClient.touch(new String[] {"abc", "zxy", "lkn"})), + Arguments.of( + "touch binary", + clusterClient.touch(new GlideString[] {gs("abc"), gs("zxy"), gs("lkn")})), + Arguments.of("watch", clusterClient.watch(new String[] {"ghi", "zxy", "lkn"}))); + } + + @SneakyThrows + @ParameterizedTest(name = "{0} cross slot keys are allowed") + @MethodSource("callCrossSlotCommandsWhichShouldPass") + public void check_does_not_throw_cross_slot_error(String testName, CompletableFuture future) { + future.get(); + } + + @Test + @SneakyThrows + public void flushall() { + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + assertEquals(OK, clusterClient.flushall(SYNC).get()); + } else { + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.flushall(SYNC).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertEquals(OK, clusterClient.flushall(ASYNC).get()); + } + + // TODO replace with KEYS command when implemented + Object[] keysAfter = + (Object[]) clusterClient.customCommand(new String[] {"keys", "*"}).get().getSingleValue(); + assertEquals(0, keysAfter.length); + + var route = new SlotKeyRoute("key", PRIMARY); + assertEquals(OK, clusterClient.flushall().get()); + assertEquals(OK, clusterClient.flushall(route).get()); + assertEquals(OK, clusterClient.flushall(ASYNC).get()); + assertEquals(OK, clusterClient.flushall(ASYNC, route).get()); + + var replicaRoute = new SlotKeyRoute("key", REPLICA); + // command should fail on a replica, because it is read-only + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.flushall(replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .toLowerCase() + .contains("can't write against a read only replica")); + } + + // TODO: add a binary version of this test + @SneakyThrows + @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") + @ValueSource(booleans = {true, false}) + public void function_commands_without_keys_with_route(boolean singleNodeRoute) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "mylib1c_" + singleNodeRoute; + String funcName = "myfunc1c_" + singleNodeRoute; + // function $funcName returns first argument + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true); + Route route = singleNodeRoute ? new SlotKeyRoute("1", PRIMARY) : ALL_PRIMARIES; + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); + + var fcallResult = clusterClient.fcall(funcName, new String[] {"one", "two"}, route).get(); + if (route instanceof SingleNodeRoute) { + assertEquals("one", fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals("one", nodeResponse); + } + } + fcallResult = clusterClient.fcallReadOnly(funcName, new String[] {"one", "two"}, route).get(); + if (route instanceof SingleNodeRoute) { + assertEquals("one", fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals("one", nodeResponse); + } + } + + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of("no-writes")); + } + }; + + var response = clusterClient.functionList(false, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } + } + + response = clusterClient.functionList(true, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + } + } + + // re-load library without overwriting + var executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionLoad(code, false, route).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + String newFuncName = "myfunc2c_" + singleNodeRoute; + // function $funcName returns first argument + // function $newFuncName returns argument array len + String newCode = + generateLuaLibCode( + libName, Map.of(funcName, "return args[1]", newFuncName, "return #args"), true); + + assertEquals(libName, clusterClient.functionLoad(newCode, true, route).get()); + + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of("no-writes")); + + response = clusterClient.functionList(false, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } + } + + // load new lib and delete it - first lib remains loaded + String anotherLib = generateLuaLibCode("anotherLib", Map.of("anotherFunc", ""), false); + assertEquals("anotherLib", clusterClient.functionLoad(anotherLib, true, route).get()); + assertEquals(OK, clusterClient.functionDelete("anotherLib", route).get()); + + // delete missing lib returns a error + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.functionDelete("anotherLib", route).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library not found")); + + response = clusterClient.functionList(true, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + } + } + + fcallResult = clusterClient.fcall(newFuncName, new String[] {"one", "two"}, route).get(); + if (route instanceof SingleNodeRoute) { + assertEquals(2L, fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals(2L, nodeResponse); + } + } + fcallResult = + clusterClient.fcallReadOnly(newFuncName, new String[] {"one", "two"}, route).get(); + if (route instanceof SingleNodeRoute) { + assertEquals(2L, fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals(2L, nodeResponse); + } + } + + assertEquals(OK, clusterClient.functionFlush(route).get()); + } + + // TODO: add a binary version of this test + @SneakyThrows + @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") + @ValueSource(booleans = {true, false}) + public void function_commands_without_keys_with_route_binary(boolean singleNodeRoute) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("mylib1c_" + singleNodeRoute); + GlideString funcName = gs("myfunc1c_" + singleNodeRoute); + // function $funcName returns first argument + GlideString code = + generateLuaLibCodeBinary(libName, Map.of(funcName, gs("return args[1]")), true); + Route route = singleNodeRoute ? new SlotKeyRoute("1", PRIMARY) : ALL_PRIMARIES; + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); + + var fcallResult = + clusterClient.fcall(funcName, new GlideString[] {gs("one"), gs("two")}, route).get(); + if (route instanceof SingleNodeRoute) { + assertEquals(gs("one"), fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals(gs("one"), nodeResponse); + } + } + fcallResult = + clusterClient + .fcallReadOnly(funcName, new GlideString[] {gs("one"), gs("two")}, route) + .get(); + if (route instanceof SingleNodeRoute) { + assertEquals(gs("one"), fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals(gs("one"), nodeResponse); + } + } + + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of(gs("no-writes"))); + } + }; + + var response = clusterClient.functionListBinary(false, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } + } + + response = clusterClient.functionListBinary(true, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + } + } + + // re-load library without overwriting + var executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionLoad(code, false, route).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + GlideString newFuncName = gs("myfunc2c_" + singleNodeRoute); + // function $funcName returns first argument + // function $newFuncName returns argument array len + GlideString newCode = + generateLuaLibCodeBinary( + libName, Map.of(funcName, gs("return args[1]"), newFuncName, gs("return #args")), true); + + assertEquals(libName, clusterClient.functionLoad(newCode, true, route).get()); + + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of(gs("no-writes"))); + + response = clusterClient.functionListBinary(false, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + } + } + + // load new lib and delete it - first lib remains loaded + GlideString anotherLib = + generateLuaLibCodeBinary(gs("anotherLib"), Map.of(gs("anotherFunc"), gs("")), false); + assertEquals(gs("anotherLib"), clusterClient.functionLoad(anotherLib, true, route).get()); + assertEquals(OK, clusterClient.functionDelete(gs("anotherLib"), route).get()); + + // delete missing lib returns a error + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.functionDelete(gs("anotherLib"), route).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library not found")); + + response = clusterClient.functionListBinary(true, route).get(); + if (singleNodeRoute) { + var flist = response.getSingleValue(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + } else { + for (var flist : response.getMultiValue().values()) { + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + } + } + + fcallResult = + clusterClient.fcall(newFuncName, new GlideString[] {gs("one"), gs("two")}, route).get(); + if (route instanceof SingleNodeRoute) { + assertEquals(2L, fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals(2L, nodeResponse); + } + } + fcallResult = + clusterClient + .fcallReadOnly(newFuncName, new GlideString[] {gs("one"), gs("two")}, route) + .get(); + if (route instanceof SingleNodeRoute) { + assertEquals(2L, fcallResult.getSingleValue()); + } else { + for (var nodeResponse : fcallResult.getMultiValue().values()) { + assertEquals(2L, nodeResponse); + } + } + + assertEquals(OK, clusterClient.functionFlush(route).get()); + } + + @SneakyThrows + @Test + public void function_commands_without_keys_and_without_route() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + String libName = "mylib1c"; + String funcName = "myfunc1c"; + // function $funcName returns first argument + // generating RO functions to execution on a replica (default routing goes to RANDOM including + // replicas) + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true); + + assertEquals(libName, clusterClient.functionLoad(code, false).get()); + + assertEquals("one", clusterClient.fcall(funcName, new String[] {"one", "two"}).get()); + assertEquals("one", clusterClient.fcallReadOnly(funcName, new String[] {"one", "two"}).get()); + + var flist = clusterClient.functionList(false).get(); + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of("no-writes")); + } + }; + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = clusterClient.functionList(true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + + // re-load library without overwriting + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.functionLoad(code, false).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + String newFuncName = "myfunc2c"; + // function $funcName returns first argument + // function $newFuncName returns argument array len + String newCode = + generateLuaLibCode( + libName, Map.of(funcName, "return args[1]", newFuncName, "return #args"), true); + + assertEquals(libName, clusterClient.functionLoad(newCode, true).get()); + + // load new lib and delete it - first lib remains loaded + String anotherLib = generateLuaLibCode("anotherLib", Map.of("anotherFunc", ""), false); + assertEquals("anotherLib", clusterClient.functionLoad(anotherLib, true).get()); + assertEquals(OK, clusterClient.functionDelete("anotherLib").get()); + + // delete missing lib returns a error + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionDelete("anotherLib").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library not found")); + + flist = clusterClient.functionList(libName, false).get(); + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of("no-writes")); + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = clusterClient.functionList(libName, true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + + assertEquals(2L, clusterClient.fcall(newFuncName, new String[] {"one", "two"}).get()); + assertEquals(2L, clusterClient.fcallReadOnly(newFuncName, new String[] {"one", "two"}).get()); + + assertEquals(OK, clusterClient.functionFlush(ASYNC).get()); + } + + @SneakyThrows + @Test + public void function_commands_without_keys_and_without_route_binary() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + GlideString libName = gs("mylib1c"); + GlideString funcName = gs("myfunc1c"); + // function $funcName returns first argument + // generating RO functions to execution on a replica (default routing goes to RANDOM including + // replicas) + GlideString code = + generateLuaLibCodeBinary(libName, Map.of(funcName, gs("return args[1]")), true); + + assertEquals(libName, clusterClient.functionLoad(code, false).get()); + + assertEquals( + gs("one"), clusterClient.fcall(funcName, new GlideString[] {gs("one"), gs("two")}).get()); + assertEquals( + gs("one"), + clusterClient.fcallReadOnly(funcName, new GlideString[] {gs("one"), gs("two")}).get()); + + var flist = clusterClient.functionListBinary(false).get(); + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of(gs("no-writes"))); + } + }; + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = clusterClient.functionListBinary(true).get(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + + // re-load library without overwriting + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.functionLoad(code, false).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + GlideString newFuncName = gs("myfunc2c"); + // function $funcName returns first argument + // function $newFuncName returns argument array len + GlideString newCode = + generateLuaLibCodeBinary( + libName, Map.of(funcName, gs("return args[1]"), newFuncName, gs("return #args")), true); + + assertEquals(libName, clusterClient.functionLoad(newCode, true).get()); + + // load new lib and delete it - first lib remains loaded + GlideString anotherLib = + generateLuaLibCodeBinary(gs("anotherLib"), Map.of(gs("anotherFunc"), gs("")), false); + assertEquals(gs("anotherLib"), clusterClient.functionLoad(anotherLib, true).get()); + assertEquals(OK, clusterClient.functionDelete(gs("anotherLib")).get()); + + // delete missing lib returns a error + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionDelete(gs("anotherLib")).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library not found")); + + flist = clusterClient.functionListBinary(libName, false).get(); + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of(gs("no-writes"))); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = clusterClient.functionListBinary(libName, true).get(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + + assertEquals( + 2L, clusterClient.fcall(newFuncName, new GlideString[] {gs("one"), gs("two")}).get()); + assertEquals( + 2L, + clusterClient.fcallReadOnly(newFuncName, new GlideString[] {gs("one"), gs("two")}).get()); + + assertEquals(OK, clusterClient.functionFlush(ASYNC).get()); + } + + @ParameterizedTest + @ValueSource(strings = {"abc", "xyz", "kln"}) + @SneakyThrows + public void fcall_with_keys(String prefix) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String key = "{" + prefix + "}-fcall_with_keys-"; + SingleNodeRoute route = new SlotKeyRoute(key, PRIMARY); + String libName = "mylib_with_keys"; + String funcName = "myfunc_with_keys"; + // function $funcName returns array with first two arguments + String code = generateLuaLibCode(libName, Map.of(funcName, "return {keys[1], keys[2]}"), true); + + // loading function to the node where key is stored + assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); + + // due to common prefix, all keys are mapped to the same hash slot + var functionResult = + clusterClient.fcall(funcName, new String[] {key + 1, key + 2}, new String[0]).get(); + assertArrayEquals(new Object[] {key + 1, key + 2}, (Object[]) functionResult); + functionResult = + clusterClient.fcallReadOnly(funcName, new String[] {key + 1, key + 2}, new String[0]).get(); + assertArrayEquals(new Object[] {key + 1, key + 2}, (Object[]) functionResult); + + var transaction = + new ClusterTransaction() + .fcall(funcName, new String[] {key + 1, key + 2}, new String[0]) + .fcallReadOnly(funcName, new String[] {key + 1, key + 2}, new String[0]); + + // check response from a routed transaction request + assertDeepEquals( + new Object[][] {{key + 1, key + 2}, {key + 1, key + 2}}, + clusterClient.exec(transaction, route).get()); + // if no route given, GLIDE should detect it automatically + assertDeepEquals( + new Object[][] {{key + 1, key + 2}, {key + 1, key + 2}}, + clusterClient.exec(transaction).get()); + + assertEquals(OK, clusterClient.functionDelete(libName, route).get()); + } + + @ParameterizedTest + @ValueSource(strings = {"abc", "xyz", "kln"}) + @SneakyThrows + public void fcall_binary_with_keys(String prefix) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String key = "{" + prefix + "}-fcall_with_keys-"; + SingleNodeRoute route = new SlotKeyRoute(key, PRIMARY); + String libName = "mylib_with_keys"; + GlideString funcName = gs("myfunc_with_keys"); + // function $funcName returns array with first two arguments + String code = + generateLuaLibCode(libName, Map.of(funcName.toString(), "return {keys[1], keys[2]}"), true); + + // loading function to the node where key is stored + assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); + + // due to common prefix, all keys are mapped to the same hash slot + var functionResult = + clusterClient + .fcall(funcName, new GlideString[] {gs(key + 1), gs(key + 2)}, new GlideString[0]) + .get(); + assertArrayEquals(new Object[] {gs(key + 1), gs(key + 2)}, (Object[]) functionResult); + functionResult = + clusterClient + .fcallReadOnly( + funcName, new GlideString[] {gs(key + 1), gs(key + 2)}, new GlideString[0]) + .get(); + assertArrayEquals(new Object[] {gs(key + 1), gs(key + 2)}, (Object[]) functionResult); + + // TODO: change to binary transaction version once available: + // var transaction = + // new ClusterTransaction() + // .fcall(funcName, new String[] {key + 1, key + 2}, new String[0]) + // .fcallReadOnly(funcName, new String[] {key + 1, key + 2}, new String[0]); + + // // check response from a routed transaction request + // assertDeepEquals( + // new Object[][] {{key + 1, key + 2}, {key + 1, key + 2}}, + // clusterClient.exec(transaction, route).get()); + // // if no route given, GLIDE should detect it automatically + // assertDeepEquals( + // new Object[][] {{key + 1, key + 2}, {key + 1, key + 2}}, + // clusterClient.exec(transaction).get()); + + assertEquals(OK, clusterClient.functionDelete(libName, route).get()); + } + + @SneakyThrows + @Test + public void fcall_readonly_function() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "fcall_readonly_function"; + // intentionally using a REPLICA route + Route replicaRoute = new SlotKeyRoute(libName, REPLICA); + Route primaryRoute = new SlotKeyRoute(libName, PRIMARY); + String funcName = "fcall_readonly_function"; + + // function $funcName returns a magic number + String code = generateLuaLibCode(libName, Map.of(funcName, "return 42"), false); + + assertEquals(libName, clusterClient.functionLoad(code, false).get()); + + // fcall on a replica node should fail, because a function isn't guaranteed to be RO + var executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.fcall(funcName, replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("You can't write against a read only replica.")); + + // fcall_ro also fails + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.fcallReadOnly(funcName, replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("You can't write against a read only replica.")); + + // fcall_ro also fails to run it even on primary - another error + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.fcallReadOnly(funcName, primaryRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .contains("Can not execute a script with write flag using *_ro command.")); + + // create the same function, but with RO flag + code = generateLuaLibCode(libName, Map.of(funcName, "return 42"), true); + + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + // fcall should succeed now + assertEquals(42L, clusterClient.fcall(funcName, replicaRoute).get().getSingleValue()); + + assertEquals(OK, clusterClient.functionDelete(libName).get()); + } + + @SneakyThrows + @Test + public void fcall_readonly_binary_function() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "fcall_readonly_function"; + // intentionally using a REPLICA route + Route replicaRoute = new SlotKeyRoute(libName, REPLICA); + Route primaryRoute = new SlotKeyRoute(libName, PRIMARY); + GlideString funcName = gs("fcall_readonly_function"); + + // function $funcName returns a magic number + String code = generateLuaLibCode(libName, Map.of(funcName.toString(), "return 42"), false); + + assertEquals(libName, clusterClient.functionLoad(code, false).get()); + + // fcall on a replica node should fail, because a function isn't guaranteed to be RO + var executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.fcall(funcName, replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("You can't write against a read only replica.")); + + // fcall_ro also fails + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.fcallReadOnly(funcName, replicaRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("You can't write against a read only replica.")); + + // fcall_ro also fails to run it even on primary - another error + executionException = + assertThrows( + ExecutionException.class, + () -> clusterClient.fcallReadOnly(funcName, primaryRoute).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException + .getMessage() + .contains("Can not execute a script with write flag using *_ro command.")); + + // create the same function, but with RO flag + code = generateLuaLibCode(libName, Map.of(funcName.toString(), "return 42"), true); + + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + // fcall should succeed now + assertEquals(42L, clusterClient.fcall(funcName, replicaRoute).get().getSingleValue()); + + assertEquals(OK, clusterClient.functionDelete(libName).get()); + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKill_no_write_without_route() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionKill_no_write_without_route"; + String funcName = "deadlock_without_route"; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, true); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // call the function without await + // Using a random primary node route, otherwise FCALL can go to a replica. + // FKILL and FSTATS go to primary nodes if no route given, test fails in such case. + Route route = new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY); + testClient.fcall(funcName, route); + + Thread.sleep(1000); + + // Run FKILL until it returns OK + boolean functionKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, clusterClient.functionKill().get()); + functionKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(functionKilled); + } finally { + waitForNotBusy(clusterClient); + } + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKillBinary_no_write_without_route() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionKillBinary_no_write_without_route"); + GlideString funcName = gs("deadlock_without_route"); + GlideString code = + gs(createLuaLibWithLongRunningFunction(libName.toString(), funcName.toString(), 6, true)); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // call the function without await + // Using a random primary node route, otherwise FCALL can go to a replica. + // FKILL go to primary nodes if no route given, test fails in such case. + Route route = new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY); + testClient.fcall(funcName, route); + + Thread.sleep(1000); + + // Run FKILL until it returns OK + boolean functionKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, clusterClient.functionKill().get()); + functionKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(functionKilled); + } finally { + waitForNotBusy(clusterClient); + } + } + } + + @Timeout(20) + @ParameterizedTest(name = "single node route = {0}") + @ValueSource(booleans = {true, false}) + @SneakyThrows + public void functionKill_no_write_with_route(boolean singleNodeRoute) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionKill_no_write_with_route" + singleNodeRoute; + String funcName = "deadlock_with_route_" + singleNodeRoute; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, true); + Route route = + singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // call the function without await + testClient.fcall(funcName, route); + + Thread.sleep(1000); + boolean functionKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, clusterClient.functionKill().get()); + functionKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(functionKilled); + } finally { + waitForNotBusy(clusterClient); + } + } + } + + @Timeout(20) + @ParameterizedTest(name = "single node route = {0}") + @ValueSource(booleans = {true, false}) + @SneakyThrows + public void functionKillBinary_no_write_with_route(boolean singleNodeRoute) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionKillBinary_no_write_with_route" + singleNodeRoute); + GlideString funcName = gs("deadlock_with_route_" + singleNodeRoute); + GlideString code = + gs(createLuaLibWithLongRunningFunction(libName.toString(), funcName.toString(), 6, true)); + Route route = + singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // call the function without await + testClient.fcall(funcName, route); + + Thread.sleep(1000); + + boolean functionKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, clusterClient.functionKill().get()); + functionKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(functionKilled); + } finally { + waitForNotBusy(clusterClient); + } + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKill_key_based_write_function() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionKill_key_based_write_function"; + String funcName = "deadlock_write_function_with_key_based_route"; + String key = libName; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, false); + Route route = new SlotKeyRoute(key, PRIMARY); + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // call the function without await + promise = testClient.fcall(funcName, new String[] {key}, new String[0]); + + Thread.sleep(1000); + + boolean foundUnkillable = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + // valkey kills a function with 5 sec delay + // but this will always throw an error in the test + clusterClient.functionKill(route).get(); + } catch (ExecutionException executionException) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if (executionException.getCause() instanceof RequestException + && executionException.getMessage().toLowerCase().contains("unkillable")) { + foundUnkillable = true; + break; + } + } + Thread.sleep(500); + timeout -= 500; + } + assertTrue(foundUnkillable); + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + // wait for the function to complete (we cannot kill it) + try { + promise.get(); + } catch (Exception ignored) { + } + } + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKillBinary_key_based_write_function() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionKillBinary_key_based_write_function"); + GlideString funcName = gs("deadlock_write_function_with_key_based_route"); + GlideString key = libName; + GlideString code = + gs(createLuaLibWithLongRunningFunction(libName.toString(), funcName.toString(), 6, false)); + Route route = new SlotKeyRoute(key.toString(), PRIMARY); + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> clusterClient.functionKill(route).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + try (var testClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(10000).build()) + .get()) { + try { + // call the function without await + promise = testClient.fcall(funcName, new GlideString[] {key}, new GlideString[0]); + + Thread.sleep(1000); + + boolean foundUnkillable = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + // valkey kills a function with 5 sec delay + // but this will always throw an error in the test + clusterClient.functionKill(route).get(); + } catch (ExecutionException executionException) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if (executionException.getCause() instanceof RequestException + && executionException.getMessage().toLowerCase().contains("unkillable")) { + foundUnkillable = true; + break; + } + } + Thread.sleep(500); + timeout -= 500; + } + assertTrue(foundUnkillable); + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + // wait for the function to complete (we cannot kill it) + try { + promise.get(); + } catch (Exception ignored) { + } + } + } + } + + @Test + @SneakyThrows + public void functionStats_without_route() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionStats_without_route"; + String funcName = libName; + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // function $funcName returns first argument + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), false); + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + var response = clusterClient.functionStats().get().getMultiValue(); + for (var nodeResponse : response.values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 1, 1); + } + + code = + generateLuaLibCode( + libName + "_2", + Map.of(funcName + "_2", "return 'OK'", funcName + "_3", "return 42"), + false); + assertEquals(libName + "_2", clusterClient.functionLoad(code, true).get()); + + response = clusterClient.functionStats().get().getMultiValue(); + for (var nodeResponse : response.values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 2, 3); + } + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + response = clusterClient.functionStats().get().getMultiValue(); + for (var nodeResponse : response.values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 0, 0); + } + } + + @Test + @SneakyThrows + public void functionStatsBinary_without_route() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionStats_without_route"); + GlideString funcName = libName; + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // function $funcName returns first argument + GlideString code = + generateLuaLibCodeBinary(libName, Map.of(funcName, gs("return args[1]")), false); + assertEquals(libName, clusterClient.functionLoad(code, true).get()); + + var response = clusterClient.functionStatsBinary().get().getMultiValue(); + for (var nodeResponse : response.values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 1, 1); + } + + code = + generateLuaLibCodeBinary( + gs(libName.toString() + "_2"), + Map.of( + gs(funcName.toString() + "_2"), + gs("return 'OK'"), + gs(funcName.toString() + "_3"), + gs("return 42")), + false); + assertEquals(gs(libName.toString() + "_2"), clusterClient.functionLoad(code, true).get()); + + response = clusterClient.functionStatsBinary().get().getMultiValue(); + for (var nodeResponse : response.values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 2, 3); + } + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + response = clusterClient.functionStatsBinary().get().getMultiValue(); + for (var nodeResponse : response.values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 0, 0); + } + } + + @ParameterizedTest(name = "single node route = {0}") + @ValueSource(booleans = {true, false}) + @SneakyThrows + public void functionStats_with_route(boolean singleNodeRoute) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + Route route = + singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; + String libName = "functionStats_with_route_" + singleNodeRoute; + String funcName = libName; + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + // function $funcName returns first argument + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), false); + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + var response = clusterClient.functionStats(route).get(); + if (singleNodeRoute) { + checkFunctionStatsResponse(response.getSingleValue(), new String[0], 1, 1); + } else { + for (var nodeResponse : response.getMultiValue().values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 1, 1); + } + } + + code = + generateLuaLibCode( + libName + "_2", + Map.of(funcName + "_2", "return 'OK'", funcName + "_3", "return 42"), + false); + assertEquals(libName + "_2", clusterClient.functionLoad(code, true, route).get()); + + response = clusterClient.functionStats(route).get(); + if (singleNodeRoute) { + checkFunctionStatsResponse(response.getSingleValue(), new String[0], 2, 3); + } else { + for (var nodeResponse : response.getMultiValue().values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 2, 3); + } + } + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + response = clusterClient.functionStats(route).get(); + if (singleNodeRoute) { + checkFunctionStatsResponse(response.getSingleValue(), new String[0], 0, 0); + } else { + for (var nodeResponse : response.getMultiValue().values()) { + checkFunctionStatsResponse(nodeResponse, new String[0], 0, 0); + } + } + } + + @ParameterizedTest(name = "single node route = {0}") + @ValueSource(booleans = {true, false}) + @SneakyThrows + public void functionStatsBinary_with_route(boolean singleNodeRoute) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + Route route = + singleNodeRoute ? new SlotKeyRoute(UUID.randomUUID().toString(), PRIMARY) : ALL_PRIMARIES; + GlideString libName = gs("functionStats_with_route_" + singleNodeRoute); + GlideString funcName = libName; + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + // function $funcName returns first argument + GlideString code = + generateLuaLibCodeBinary(libName, Map.of(funcName, gs("return args[1]")), false); + assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); + + var response = clusterClient.functionStatsBinary(route).get(); + if (singleNodeRoute) { + checkFunctionStatsBinaryResponse(response.getSingleValue(), new GlideString[0], 1, 1); + } else { + for (var nodeResponse : response.getMultiValue().values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 1, 1); + } + } + + code = + generateLuaLibCodeBinary( + gs(libName.toString() + "_2"), + Map.of( + gs(funcName.toString() + "_2"), + gs("return 'OK'"), + gs(funcName.toString() + "_3"), + gs("return 42")), + false); + assertEquals( + gs(libName.toString() + "_2"), clusterClient.functionLoad(code, true, route).get()); + + response = clusterClient.functionStatsBinary(route).get(); + if (singleNodeRoute) { + checkFunctionStatsBinaryResponse(response.getSingleValue(), new GlideString[0], 2, 3); + } else { + for (var nodeResponse : response.getMultiValue().values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 2, 3); + } + } + + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); + + response = clusterClient.functionStatsBinary(route).get(); + if (singleNodeRoute) { + checkFunctionStatsBinaryResponse(response.getSingleValue(), new GlideString[0], 0, 0); + } else { + for (var nodeResponse : response.getMultiValue().values()) { + checkFunctionStatsBinaryResponse(nodeResponse, new GlideString[0], 0, 0); + } + } + } + + @Test + @SneakyThrows + public void function_dump_and_restore() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // dumping an empty lib + byte[] emptyDump = clusterClient.functionDump().get(); + assertTrue(emptyDump.length > 0); + + String name1 = "Foster"; + String libname1 = "FosterLib"; + String name2 = "Dogster"; + String libname2 = "DogsterLib"; + + // function $name1 returns first argument + // function $name2 returns argument array len + String code = + generateLuaLibCode(libname1, Map.of(name1, "return args[1]", name2, "return #args"), true); + assertEquals(libname1, clusterClient.functionLoad(code, true).get()); + Map[] flist = clusterClient.functionList(true).get(); + + final byte[] dump = clusterClient.functionDump().get(); + + // restore without cleaning the lib and/or overwrite option causes an error + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.functionRestore(dump).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + libname1 + " already exists")); + + // APPEND policy also fails for the same reason (name collision) + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionRestore(dump, APPEND).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + libname1 + " already exists")); + + // REPLACE policy succeeds + assertEquals(OK, clusterClient.functionRestore(dump, REPLACE).get()); + // but nothing changed - all code overwritten + var restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname1, restoredFunctionList[0].get("library_name")); + // Note that function ordering may differ across nodes so we can't do a deep equals + assertEquals(2, ((Object[]) restoredFunctionList[0].get("functions")).length); + + // create lib with another name, but with the same function names + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + code = + generateLuaLibCode(libname2, Map.of(name1, "return args[1]", name2, "return #args"), true); + assertEquals(libname2, clusterClient.functionLoad(code, true).get()); + restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname2, restoredFunctionList[0].get("library_name")); + + // REPLACE policy now fails due to a name collision + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionRestore(dump, REPLACE).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // valkey checks names in random order and blames on first collision + assertTrue( + executionException.getMessage().contains("Function " + name1 + " already exists") + || executionException.getMessage().contains("Function " + name2 + " already exists")); + + // FLUSH policy succeeds, but deletes the second lib + assertEquals(OK, clusterClient.functionRestore(dump, FLUSH).get()); + restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname1, restoredFunctionList[0].get("library_name")); + // Note that function ordering may differ across nodes + assertEquals(2, ((Object[]) restoredFunctionList[0].get("functions")).length); + + // call restored functions + assertEquals( + "meow", + clusterClient.fcallReadOnly(name1, new String[0], new String[] {"meow", "woem"}).get()); + assertEquals( + 2L, clusterClient.fcallReadOnly(name2, new String[0], new String[] {"meow", "woem"}).get()); + } + + @Test + @SneakyThrows + public void randomKey() { + String key1 = "{key}" + UUID.randomUUID(); + String key2 = "{key}" + UUID.randomUUID(); + + assertEquals(OK, clusterClient.set(key1, "a").get()); + assertEquals(OK, clusterClient.set(key2, "b").get()); + + String randomKey = clusterClient.randomKey().get(); + assertEquals(1L, clusterClient.exists(new String[] {randomKey}).get()); + + String randomKeyPrimaries = clusterClient.randomKey(ALL_PRIMARIES).get(); + assertEquals(1L, clusterClient.exists(new String[] {randomKeyPrimaries}).get()); + + // no keys in database + assertEquals(OK, clusterClient.flushall(SYNC).get()); + + // no keys in database returns null + assertNull(clusterClient.randomKey().get()); + } + + @Test + @SneakyThrows + public void randomKeyBinary() { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + + assertEquals(OK, clusterClient.set(key1, gs("a")).get()); + assertEquals(OK, clusterClient.set(key2, gs("b")).get()); + + GlideString randomKey = clusterClient.randomKeyBinary().get(); + assertEquals(1L, clusterClient.exists(new GlideString[] {randomKey}).get()); + + GlideString randomKeyPrimaries = clusterClient.randomKeyBinary(ALL_PRIMARIES).get(); + assertEquals(1L, clusterClient.exists(new GlideString[] {randomKeyPrimaries}).get()); + + // no keys in database + assertEquals(OK, clusterClient.flushall(SYNC).get()); + + // no keys in database returns null + assertNull(clusterClient.randomKey().get()); + } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key1DescendingList = {"4", "3", "2", "1"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + String[] key2DescendingList = {"x", "c", "a", "4", "3", "2", "1"}; + String[] key2DescendingListSubset = Arrays.copyOfRange(key2DescendingList, 0, 4); + + assertArrayEquals(new String[0], clusterClient.sort(key3).get()); + assertEquals(4, clusterClient.lpush(key1, key1LpushArgs).get()); + assertArrayEquals( + new String[0], + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 0L)).build()) + .get()); + assertArrayEquals( + key1DescendingList, + clusterClient.sort(key1, SortClusterOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertEquals(7, clusterClient.lpush(key2, key2LpushArgs).get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sort( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + + // SORT_R0 + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + key1DescendingList, + clusterClient + .sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sortReadOnly( + key1, + SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sortReadOnly( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + } + + // SORT with STORE + assertEquals( + 4, + clusterClient + .sortStore( + key2, + key3, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + assertArrayEquals(key2DescendingListSubset, clusterClient.lrange(key3, 0, -1).get()); + } + + @Test + @SneakyThrows + public void sort_binary() { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString[] key1LpushArgs = {gs("2"), gs("1"), gs("4"), gs("3")}; + GlideString[] key1AscendingList = {gs("1"), gs("2"), gs("3"), gs("4")}; + GlideString[] key1DescendingList = {gs("4"), gs("3"), gs("2"), gs("1")}; + GlideString[] key2LpushArgs = {gs("2"), gs("1"), gs("a"), gs("x"), gs("c"), gs("4"), gs("3")}; + GlideString[] key2DescendingList = { + gs("x"), gs("c"), gs("a"), gs("4"), gs("3"), gs("2"), gs("1") + }; + String[] key2DescendingList_strings = {"x", "c", "a", "4", "3", "2", "1"}; + GlideString[] key2DescendingListSubset = Arrays.copyOfRange(key2DescendingList, 0, 4); + String[] key2DescendingListSubset_strings = + Arrays.copyOfRange(key2DescendingList_strings, 0, 4); + + assertArrayEquals(new GlideString[0], clusterClient.sort(key3).get()); + assertEquals(4, clusterClient.lpush(key1, key1LpushArgs).get()); + assertArrayEquals( + new GlideString[0], + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 0L)).build()) + .get()); + assertArrayEquals( + key1DescendingList, + clusterClient.sort(key1, SortClusterOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertEquals(7, clusterClient.lpush(key2, key2LpushArgs).get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sort( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + + // SORT_R0 + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + key1DescendingList, + clusterClient + .sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sortReadOnly( + key1, + SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sortReadOnly( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + } + + // SORT with STORE + assertEquals( + 4, + clusterClient + .sortStore( + key2, + key3, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + assertArrayEquals( + key2DescendingListSubset_strings, clusterClient.lrange(key3.toString(), 0, -1).get()); + } + + @Timeout(20) + @Test + @SneakyThrows + public void test_cluster_scan_simple() { + assertEquals(OK, clusterClient.flushall().get()); + + String key = "key:test_cluster_scan_simple" + UUID.randomUUID(); + Map expectedData = new LinkedHashMap<>(); + for (int i = 0; i < 100; i++) { + expectedData.put(key + ":" + i, "value " + i); + } + + assertEquals(OK, clusterClient.mset(expectedData).get()); + + Set result = new LinkedHashSet<>(); + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + while (!cursor.isFinished()) { + final Object[] response = clusterClient.scan(cursor).get(); + cursor.releaseCursorHandle(); + + cursor = (ClusterScanCursor) response[0]; + final Object[] data = (Object[]) response[1]; + for (Object datum : data) { + result.add(datum.toString()); + } + } + cursor.releaseCursorHandle(); + + assertEquals(expectedData.keySet(), result); + } + + @Timeout(20) + @Test + @SneakyThrows + public void test_cluster_scan_binary_simple() { + assertEquals(OK, clusterClient.flushall().get()); + + String key = "key:test_cluster_scan_simple" + UUID.randomUUID(); + Map expectedData = new LinkedHashMap<>(); + for (int i = 0; i < 100; i++) { + expectedData.put(key + ":" + i, "value " + i); + } + + assertEquals(OK, clusterClient.mset(expectedData).get()); + + Set result = new LinkedHashSet<>(); + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + while (!cursor.isFinished()) { + final Object[] response = clusterClient.scanBinary(cursor).get(); + cursor.releaseCursorHandle(); + + cursor = (ClusterScanCursor) response[0]; + final Object[] data = (Object[]) response[1]; + for (Object datum : data) { + result.add(datum.toString()); + } + } + cursor.releaseCursorHandle(); + + assertEquals(expectedData.keySet(), result); + } + + @Timeout(20) + @Test + @SneakyThrows + public void test_cluster_scan_with_object_type_and_pattern() { + assertEquals(OK, clusterClient.flushall().get()); + String key = "key:" + UUID.randomUUID(); + Map expectedData = new LinkedHashMap<>(); + final int baseNumberOfEntries = 100; + for (int i = 0; i < baseNumberOfEntries; i++) { + expectedData.put(key + ":" + i, "value " + i); + } + + assertEquals(OK, clusterClient.mset(expectedData).get()); + + ArrayList unexpectedTypeKeys = new ArrayList<>(); + for (int i = baseNumberOfEntries; i < baseNumberOfEntries + 100; i++) { + unexpectedTypeKeys.add(key + ":" + i); + } + + for (String keyStr : unexpectedTypeKeys) { + assertEquals(1L, clusterClient.sadd(keyStr, new String[] {"value"}).get()); + } + + Map unexpectedPatterns = new LinkedHashMap<>(); + for (int i = baseNumberOfEntries + 100; i < baseNumberOfEntries + 200; i++) { + unexpectedPatterns.put("foo:" + i, "value " + i); + } + assertEquals(OK, clusterClient.mset(unexpectedPatterns).get()); + + Set result = new LinkedHashSet<>(); + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + while (!cursor.isFinished()) { + final Object[] response = + clusterClient + .scan( + cursor, + ScanOptions.builder() + .matchPattern("key:*") + .type(ScanOptions.ObjectType.STRING) + .build()) + .get(); + cursor.releaseCursorHandle(); + + cursor = (ClusterScanCursor) response[0]; + final Object[] data = (Object[]) response[1]; + for (Object datum : data) { + result.add(datum.toString()); + } + } + cursor.releaseCursorHandle(); + assertEquals(expectedData.keySet(), result); + + // Ensure that no unexpected types were in the result. + assertFalse(new LinkedHashSet<>(result).removeAll(new LinkedHashSet<>(unexpectedTypeKeys))); + assertFalse(new LinkedHashSet<>(result).removeAll(unexpectedPatterns.keySet())); + } + + @Timeout(20) + @Test + @SneakyThrows + public void test_cluster_scan_with_count() { + assertEquals(OK, clusterClient.flushall().get()); + String key = "key:" + UUID.randomUUID(); + Map expectedData = new LinkedHashMap<>(); + final int baseNumberOfEntries = 2000; + for (int i = 0; i < baseNumberOfEntries; i++) { + expectedData.put(key + ":" + i, "value " + i); + } + + assertEquals(OK, clusterClient.mset(expectedData).get()); + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set keys = new LinkedHashSet<>(); + int successfulComparedScans = 0; + while (!cursor.isFinished()) { + Object[] resultOf1 = + clusterClient.scan(cursor, ScanOptions.builder().count(1L).build()).get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) resultOf1[0]; + keys.addAll( + Arrays.stream((Object[]) resultOf1[1]) + .map(Object::toString) + .collect(Collectors.toList())); + if (cursor.isFinished()) { + break; + } + + Object[] resultOf100 = + clusterClient.scan(cursor, ScanOptions.builder().count(100L).build()).get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) resultOf100[0]; + keys.addAll( + Arrays.stream((Object[]) resultOf100[1]) + .map(Object::toString) + .collect(Collectors.toList())); + + // Note: count is only an optimization hint. It does not have to return the size specified. + if (resultOf1.length <= resultOf100.length) { + successfulComparedScans++; + } + } + cursor.releaseCursorHandle(); + assertTrue(successfulComparedScans > 0); + assertEquals(expectedData.keySet(), keys); + } + + @Timeout(20) + @Test + @SneakyThrows + public void test_cluster_scan_with_match() { + assertEquals(OK, clusterClient.flushall().get()); + String key = "key:" + UUID.randomUUID(); + Map expectedData = new LinkedHashMap<>(); + final int baseNumberOfEntries = 2000; + for (int i = 0; i < baseNumberOfEntries; i++) { + expectedData.put(key + ":" + i, "value " + i); + } + assertEquals(OK, clusterClient.mset(expectedData).get()); + + Map unexpectedPatterns = new LinkedHashMap<>(); + for (int i = baseNumberOfEntries + 100; i < baseNumberOfEntries + 200; i++) { + unexpectedPatterns.put("foo:" + i, "value " + i); + } + assertEquals(OK, clusterClient.mset(unexpectedPatterns).get()); + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set keys = new LinkedHashSet<>(); + while (!cursor.isFinished()) { + Object[] result = + clusterClient.scan(cursor, ScanOptions.builder().matchPattern("key:*").build()).get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) result[0]; + keys.addAll( + Arrays.stream((Object[]) result[1]).map(Object::toString).collect(Collectors.toList())); + } + cursor.releaseCursorHandle(); + assertEquals(expectedData.keySet(), keys); + assertFalse(new LinkedHashSet<>(keys).removeAll(unexpectedPatterns.keySet())); + } + + @Timeout(20) + @Test + @SneakyThrows + public void test_cluster_scan_cleaning_cursor() { + // We test whether the cursor is cleaned up after it is deleted, which we expect to happen when + // the GC is called. + assertEquals(OK, clusterClient.flushall().get()); + + String key = "key:" + UUID.randomUUID(); + Map expectedData = new LinkedHashMap<>(); + final int baseNumberOfEntries = 100; + for (int i = 0; i < baseNumberOfEntries; i++) { + expectedData.put(key + ":" + i, "value " + i); + } + assertEquals(OK, clusterClient.mset(expectedData).get()); + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + final Object[] response = clusterClient.scan(cursor).get(); + cursor = (ClusterScanCursor) (response[0]); + cursor.releaseCursorHandle(); + final ClusterScanCursor brokenCursor = cursor; + ExecutionException exception = + assertThrows(ExecutionException.class, () -> clusterClient.scan(brokenCursor).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("Invalid scan_state_cursor id")); + } + + @Test + @SneakyThrows + public void test_cluster_scan_all_strings() { + assertEquals(OK, clusterClient.flushall().get()); + + String key = "key:" + UUID.randomUUID(); + Map stringData = new LinkedHashMap<>(); + final int baseNumberOfEntries = 5; + for (int i = 0; i < baseNumberOfEntries; i++) { + stringData.put(key + ":" + i, "value " + i); + } + assertEquals(OK, clusterClient.mset(stringData).get()); + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set results = new LinkedHashSet<>(); + while (!cursor.isFinished()) { + Object[] response = + clusterClient + .scan(cursor, ScanOptions.builder().type(ScanOptions.ObjectType.STRING).build()) + .get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) response[0]; + results.addAll( + Arrays.stream((Object[]) response[1]).map(Object::toString).collect(Collectors.toSet())); + } + cursor.releaseCursorHandle(); + assertEquals(stringData.keySet(), results); + } + + @Test + @SneakyThrows + public void test_cluster_scan_all_set() { + assertEquals(OK, clusterClient.flushall().get()); + final int baseNumberOfEntries = 5; + + String setKey = "setKey:" + UUID.randomUUID(); + Map setData = new LinkedHashMap<>(); + for (int i = 0; i < baseNumberOfEntries; i++) { + setData.put(setKey + ":" + i, "value " + i); + } + for (String k : setData.keySet()) { + assertEquals(1L, clusterClient.sadd(k, new String[] {"value" + k}).get()); + } + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set results = new LinkedHashSet<>(); + while (!cursor.isFinished()) { + Object[] response = + clusterClient + .scan(cursor, ScanOptions.builder().type(ScanOptions.ObjectType.SET).build()) + .get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) response[0]; + results.addAll( + Arrays.stream((Object[]) response[1]).map(Object::toString).collect(Collectors.toSet())); + } + cursor.releaseCursorHandle(); + assertEquals(setData.keySet(), results); + } + + @Test + @SneakyThrows + public void test_cluster_scan_all_hash() { + assertEquals(OK, clusterClient.flushall().get()); + final int baseNumberOfEntries = 5; + + String hashKey = "hashKey:" + UUID.randomUUID(); + Map hashData = new LinkedHashMap<>(); + for (int i = 0; i < baseNumberOfEntries; i++) { + hashData.put(hashKey + ":" + i, "value " + i); + } + for (String k : hashData.keySet()) { + assertEquals(1L, clusterClient.hset(k, Map.of("field" + k, "value" + k)).get()); + } + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set results = new LinkedHashSet<>(); + while (!cursor.isFinished()) { + Object[] response = + clusterClient + .scan(cursor, ScanOptions.builder().type(ScanOptions.ObjectType.HASH).build()) + .get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) response[0]; + results.addAll( + Arrays.stream((Object[]) response[1]).map(Object::toString).collect(Collectors.toSet())); + } + cursor.releaseCursorHandle(); + assertEquals(hashData.keySet(), results); + } + + @Test + @SneakyThrows + public void test_cluster_scan_all_list() { + assertEquals(OK, clusterClient.flushall().get()); + final int baseNumberOfEntries = 5; + + String listKey = "listKey:" + UUID.randomUUID(); + Map listData = new LinkedHashMap<>(); + for (int i = 0; i < baseNumberOfEntries; i++) { + listData.put(listKey + ":" + i, "value " + i); + } + for (String k : listData.keySet()) { + assertEquals(1L, clusterClient.lpush(k, new String[] {"value" + k}).get()); + } + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set results = new LinkedHashSet<>(); + while (!cursor.isFinished()) { + Object[] response = + clusterClient + .scan(cursor, ScanOptions.builder().type(ScanOptions.ObjectType.LIST).build()) + .get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) response[0]; + results.addAll( + Arrays.stream((Object[]) response[1]).map(Object::toString).collect(Collectors.toSet())); + } + cursor.releaseCursorHandle(); + assertEquals(listData.keySet(), results); + } + + @Test + @SneakyThrows + public void test_cluster_scan_all_sorted_set() { + assertEquals(OK, clusterClient.flushall().get()); + final int baseNumberOfEntries = 5; + + String zSetKey = "zSetKey:" + UUID.randomUUID(); + Map zSetData = new LinkedHashMap<>(); + for (int i = 0; i < baseNumberOfEntries; i++) { + zSetData.put(zSetKey + ":" + i, "value " + i); + } + for (String k : zSetData.keySet()) { + assertEquals(1L, clusterClient.zadd(k, Map.of(k, 1.0)).get()); + } + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set results = new LinkedHashSet<>(); + + while (!cursor.isFinished()) { + Object[] response = + clusterClient + .scan(cursor, ScanOptions.builder().type(ScanOptions.ObjectType.ZSET).build()) + .get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) response[0]; + results.addAll( + Arrays.stream((Object[]) response[1]).map(Object::toString).collect(Collectors.toSet())); + } + cursor.releaseCursorHandle(); + assertEquals(zSetData.keySet(), results); + } + + @Test + @SneakyThrows + public void test_cluster_scan_all_stream() { + assertEquals(OK, clusterClient.flushall().get()); + final int baseNumberOfEntries = 5; + + String streamKey = "streamKey:" + UUID.randomUUID(); + Map streamData = new LinkedHashMap<>(); + for (int i = 0; i < baseNumberOfEntries; i++) { + streamData.put(streamKey + ":" + i, "value " + i); + } + for (String k : streamData.keySet()) { + assertNotNull(clusterClient.xadd(k, Map.of(k, "value " + k)).get()); + } + + ClusterScanCursor cursor = ClusterScanCursor.initalCursor(); + Set results = new LinkedHashSet<>(); + + while (!cursor.isFinished()) { + Object[] response = + clusterClient + .scan(cursor, ScanOptions.builder().type(ScanOptions.ObjectType.STREAM).build()) + .get(); + cursor.releaseCursorHandle(); + cursor = (ClusterScanCursor) response[0]; + results.addAll( + Arrays.stream((Object[]) response[1]).map(Object::toString).collect(Collectors.toSet())); + } + cursor.releaseCursorHandle(); + assertEquals(streamData.keySet(), results); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 16343a2560..0a422e52dc 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -1,36 +1,73 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.standalone; -import static glide.TestConfiguration.REDIS_VERSION; -import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.assertDeepEquals; +import static glide.TestUtilities.checkFunctionListResponse; +import static glide.TestUtilities.checkFunctionListResponseBinary; +import static glide.TestUtilities.checkFunctionStatsBinaryResponse; +import static glide.TestUtilities.checkFunctionStatsResponse; +import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.createLuaLibWithLongRunningFunction; +import static glide.TestUtilities.generateLuaLibCode; +import static glide.TestUtilities.generateLuaLibCodeBinary; import static glide.TestUtilities.getValueFromInfo; +import static glide.TestUtilities.parseInfoResponseToMap; +import static glide.TestUtilities.waitForNotBusy; import static glide.api.BaseClient.OK; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.CPU; import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; +import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.function.FunctionRestorePolicy.APPEND; +import static glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; +import static glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.HASH; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.SET; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.STRING; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import glide.api.RedisClient; +import glide.api.GlideClient; +import glide.api.models.GlideString; import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.SortOptionsBinary; +import glide.api.models.commands.scan.ScanOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -40,17 +77,13 @@ public class CommandTests { private static final String INITIAL_VALUE = "VALUE"; - private static RedisClient regularClient = null; + private static GlideClient regularClient = null; @BeforeAll @SneakyThrows public static void init() { regularClient = - RedisClient.CreateClient( - RedisClientConfiguration.builder() - .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build()) - .build()) - .get(); + GlideClient.createClient(commonClientConfig().requestTimeout(7000).build()).get(); } @AfterAll @@ -59,6 +92,12 @@ public static void teardown() { regularClient.close(); } + @AfterEach + @SneakyThrows + public void cleanup() { + regularClient.flushall().get(); + } + @Test @SneakyThrows public void custom_command_info() { @@ -91,6 +130,13 @@ public void ping_with_message() { assertEquals("H3LL0", data); } + @Test + @SneakyThrows + public void ping_binary_with_message() { + GlideString data = regularClient.ping(gs("H3LL0")).get(); + assertEquals(gs("H3LL0"), data); + } + @Test @SneakyThrows public void info_without_options() { @@ -104,7 +150,7 @@ public void info_without_options() { @SneakyThrows public void info_with_multiple_options() { InfoOptions.InfoOptionsBuilder builder = InfoOptions.builder().section(CLUSTER); - if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { builder.section(CPU).section(MEMORY); } InfoOptions options = builder.build(); @@ -150,6 +196,66 @@ public void select_test_gives_error() { assertTrue(e.getCause() instanceof RequestException); } + @Test + @SneakyThrows + public void move() { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String value1 = UUID.randomUUID().toString(); + String value2 = UUID.randomUUID().toString(); + String nonExistingKey = UUID.randomUUID().toString(); + assertEquals(OK, regularClient.select(0).get()); + + assertEquals(false, regularClient.move(nonExistingKey, 1L).get()); + assertEquals(OK, regularClient.set(key1, value1).get()); + assertEquals(OK, regularClient.set(key2, value2).get()); + assertEquals(true, regularClient.move(key1, 1L).get()); + assertNull(regularClient.get(key1).get()); + + assertEquals(OK, regularClient.select(1).get()); + assertEquals(value1, regularClient.get(key1).get()); + + assertEquals(OK, regularClient.set(key2, value2).get()); + // Move does not occur because key2 already exists in DB 0 + assertEquals(false, regularClient.move(key2, 0).get()); + assertEquals(value2, regularClient.get(key2).get()); + + // Incorrect argument - DB index must be non-negative + ExecutionException e = + assertThrows(ExecutionException.class, () -> regularClient.move(key1, -1L).get()); + assertTrue(e.getCause() instanceof RequestException); + } + + @Test + @SneakyThrows + public void move_binary() { + GlideString key1 = gs(UUID.randomUUID().toString()); + GlideString key2 = gs(UUID.randomUUID().toString()); + GlideString value1 = gs(UUID.randomUUID().toString()); + GlideString value2 = gs(UUID.randomUUID().toString()); + GlideString nonExistingKey = gs(UUID.randomUUID().toString()); + assertEquals(OK, regularClient.select(0).get()); + + assertEquals(false, regularClient.move(nonExistingKey, 1L).get()); + assertEquals(OK, regularClient.set(key1, value1).get()); + assertEquals(OK, regularClient.set(key2, value2).get()); + assertEquals(true, regularClient.move(key1, 1L).get()); + assertNull(regularClient.get(key1).get()); + + assertEquals(OK, regularClient.select(1).get()); + assertEquals(value1, regularClient.get(key1).get()); + + assertEquals(OK, regularClient.set(key2, value2).get()); + // Move does not occur because key2 already exists in DB 0 + assertEquals(false, regularClient.move(key2, 0).get()); + assertEquals(value2, regularClient.get(key2).get()); + + // Incorrect argument - DB index must be non-negative + ExecutionException e = + assertThrows(ExecutionException.class, () -> regularClient.move(key1, -1L).get()); + assertTrue(e.getCause() instanceof RequestException); + } + @Test @SneakyThrows public void clientId() { @@ -185,10 +291,16 @@ public void config_reset_stat() { @Test @SneakyThrows public void config_rewrite_non_existent_config_file() { - // The setup for the Integration Tests server does not include a configuration file for Redis. - ExecutionException executionException = - assertThrows(ExecutionException.class, () -> regularClient.configRewrite().get()); - assertTrue(executionException.getCause() instanceof RequestException); + var info = regularClient.info(InfoOptions.builder().section(SERVER).build()).get(); + var configFile = parseInfoResponseToMap(info).get("config_file"); + + if (configFile.isEmpty()) { + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } else { + assertEquals(OK, regularClient.configRewrite().get()); + } } @Test @@ -213,7 +325,7 @@ public void configGet_with_wildcard() { @Test @SneakyThrows public void configGet_with_multiple_params() { - assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); var data = regularClient.configGet(new String[] {"pidfile", "logfile"}).get(); assertAll( () -> assertEquals(2, data.size()), @@ -251,6 +363,17 @@ public void echo() { String message = "GLIDE"; String response = regularClient.echo(message).get(); assertEquals(message, response); + message = ""; + response = regularClient.echo(message).get(); + assertEquals(message, response); + } + + @SneakyThrows + @Test + public void echo_gs() { + byte[] message = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + GlideString response = regularClient.echo(gs(message)).get(); + assertEquals(gs(message), response); } @Test @@ -275,4 +398,1379 @@ public void lastsave() { var yesterday = Instant.now().minus(1, ChronoUnit.DAYS); assertTrue(Instant.ofEpochSecond(result).isAfter(yesterday)); } + + @Test + @SneakyThrows + public void lolwut_lolwut() { + var response = regularClient.lolwut().get(); + System.out.printf("%nLOLWUT standalone client standard response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + response = regularClient.lolwut(new int[] {30, 4, 4}).get(); + System.out.printf( + "%nLOLWUT standalone client standard response with params 30 4 4%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + response = regularClient.lolwut(5).get(); + System.out.printf("%nLOLWUT standalone client ver 5 response%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + + response = regularClient.lolwut(6, new int[] {50, 20}).get(); + System.out.printf( + "%nLOLWUT standalone client ver 6 response with params 50 20%n%s%n", response); + assertTrue(response.contains("Redis ver. " + SERVER_VERSION)); + } + + @Test + @SneakyThrows + public void dbsize_and_flushdb() { + assertEquals(OK, regularClient.flushall().get()); + assertEquals(OK, regularClient.select(0).get()); + + // fill DB and check size + int numKeys = 10; + for (int i = 0; i < numKeys; i++) { + assertEquals(OK, regularClient.set(UUID.randomUUID().toString(), "foo").get()); + } + assertEquals(10L, regularClient.dbsize().get()); + + // check another empty DB + assertEquals(OK, regularClient.select(1).get()); + assertEquals(0L, regularClient.dbsize().get()); + + // check non-empty + assertEquals(OK, regularClient.set(UUID.randomUUID().toString(), "foo").get()); + assertEquals(1L, regularClient.dbsize().get()); + + // flush and check again + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + assertEquals(OK, regularClient.flushdb(SYNC).get()); + } else { + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.flushdb(SYNC).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertEquals(OK, regularClient.flushdb(ASYNC).get()); + } + assertEquals(0L, regularClient.dbsize().get()); + + // switch to DB 0 and flush and check + assertEquals(OK, regularClient.select(0).get()); + assertEquals(10L, regularClient.dbsize().get()); + assertEquals(OK, regularClient.flushdb().get()); + assertEquals(0L, regularClient.dbsize().get()); + } + + @Test + @SneakyThrows + public void objectFreq() { + String key = UUID.randomUUID().toString(); + String maxmemoryPolicy = "maxmemory-policy"; + String oldPolicy = + regularClient.configGet(new String[] {maxmemoryPolicy}).get().get(maxmemoryPolicy); + try { + assertEquals(OK, regularClient.configSet(Map.of(maxmemoryPolicy, "allkeys-lfu")).get()); + assertEquals(OK, regularClient.set(key, "").get()); + assertTrue(regularClient.objectFreq(key).get() >= 0L); + } finally { + regularClient.configSet(Map.of(maxmemoryPolicy, oldPolicy)).get(); + } + } + + @Test + @SneakyThrows + public void flushall() { + if (SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + assertEquals(OK, regularClient.flushall(SYNC).get()); + } else { + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.flushall(SYNC).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertEquals(OK, regularClient.flushall(ASYNC).get()); + } + + // TODO replace with KEYS command when implemented + Object[] keysAfter = (Object[]) regularClient.customCommand(new String[] {"keys", "*"}).get(); + assertEquals(0, keysAfter.length); + + assertEquals(OK, regularClient.flushall().get()); + assertEquals(OK, regularClient.flushall(ASYNC).get()); + } + + @SneakyThrows + @Test + public void function_commands() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + String libName = "mylib1c"; + String funcName = "myfunc1c"; + // function $funcName returns first argument + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true); + assertEquals(libName, regularClient.functionLoad(code, false).get()); + + var functionResult = + regularClient.fcall(funcName, new String[0], new String[] {"one", "two"}).get(); + assertEquals("one", functionResult); + functionResult = + regularClient.fcallReadOnly(funcName, new String[0], new String[] {"one", "two"}).get(); + assertEquals("one", functionResult); + + var flist = regularClient.functionList(false).get(); + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of("no-writes")); + } + }; + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = regularClient.functionList(true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + + // re-load library without overwriting + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.functionLoad(code, false).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, regularClient.functionLoad(code, true).get()); + String newFuncName = "myfunc2c"; + // function $funcName returns first argument + // function $newFuncName returns argument array len + String newCode = + generateLuaLibCode( + libName, Map.of(funcName, "return args[1]", newFuncName, "return #args"), true); + assertEquals(libName, regularClient.functionLoad(newCode, true).get()); + + // load new lib and delete it - first lib remains loaded + String anotherLib = generateLuaLibCode("anotherLib", Map.of("anotherFunc", ""), false); + assertEquals("anotherLib", regularClient.functionLoad(anotherLib, true).get()); + assertEquals(OK, regularClient.functionDelete("anotherLib").get()); + + // delete missing lib returns a error + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionDelete("anotherLib").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library not found")); + + flist = regularClient.functionList(libName, false).get(); + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of("no-writes")); + checkFunctionListResponse(flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = regularClient.functionList(libName, true).get(); + checkFunctionListResponse( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + + functionResult = + regularClient.fcall(newFuncName, new String[0], new String[] {"one", "two"}).get(); + assertEquals(2L, functionResult); + functionResult = + regularClient.fcallReadOnly(newFuncName, new String[0], new String[] {"one", "two"}).get(); + assertEquals(2L, functionResult); + + assertEquals(OK, regularClient.functionFlush(ASYNC).get()); + } + + @SneakyThrows + @Test + public void function_commands_binary() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + GlideString libName = gs("mylib1c"); + GlideString funcName = gs("myfunc1c"); + // function $funcName returns first argument + GlideString code = + generateLuaLibCodeBinary(libName, Map.of(funcName, gs("return args[1]")), true); + assertEquals(libName, regularClient.functionLoad(code, false).get()); + + var functionResult = + regularClient + .fcall(funcName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) + .get(); + assertEquals(gs("one"), functionResult); + functionResult = + regularClient + .fcallReadOnly(funcName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) + .get(); + assertEquals(gs("one"), functionResult); + + var flist = regularClient.functionListBinary(false).get(); + var expectedDescription = + new HashMap() { + { + put(funcName, null); + } + }; + var expectedFlags = + new HashMap>() { + { + put(funcName, Set.of(gs("no-writes"))); + } + }; + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = regularClient.functionListBinary(true).get(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(code)); + + // re-load library without overwriting + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.functionLoad(code, false).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue( + executionException.getMessage().contains("Library '" + libName + "' already exists")); + + // re-load library with overwriting + assertEquals(libName, regularClient.functionLoad(code, true).get()); + GlideString newFuncName = gs("myfunc2c"); + // function $funcName returns first argument + // function $newFuncName returns argument array len + GlideString newCode = + generateLuaLibCodeBinary( + libName, Map.of(funcName, gs("return args[1]"), newFuncName, gs("return #args")), true); + assertEquals(libName, regularClient.functionLoad(newCode, true).get()); + + // load new lib and delete it - first lib remains loaded + GlideString anotherLib = + generateLuaLibCodeBinary(gs("anotherLib"), Map.of(gs("anotherFunc"), gs("")), false); + assertEquals(gs("anotherLib"), regularClient.functionLoad(anotherLib, true).get()); + assertEquals(OK, regularClient.functionDelete(gs("anotherLib")).get()); + + // delete missing lib returns a error + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionDelete(gs("anotherLib")).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library not found")); + + flist = regularClient.functionListBinary(libName, false).get(); + expectedDescription.put(newFuncName, null); + expectedFlags.put(newFuncName, Set.of(gs("no-writes"))); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.empty()); + + flist = regularClient.functionListBinary(libName, true).get(); + checkFunctionListResponseBinary( + flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); + + functionResult = + regularClient + .fcall(newFuncName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) + .get(); + assertEquals(2L, functionResult); + functionResult = + regularClient + .fcallReadOnly( + newFuncName, new GlideString[0], new GlideString[] {gs("one"), gs("two")}) + .get(); + assertEquals(2L, functionResult); + + assertEquals(OK, regularClient.functionFlush(ASYNC).get()); + } + + @Test + @SneakyThrows + public void copy() { + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in version 6.2.0"); + // setup + String source = "{key}-1" + UUID.randomUUID(); + String destination = "{key}-2" + UUID.randomUUID(); + long index1 = 1; + long index2 = 2; + + try { + // neither key exists, returns false + assertFalse(regularClient.copy(source, destination, index1, false).get()); + + // source exists, destination does not + regularClient.set(source, "one").get(); + assertTrue(regularClient.copy(source, destination, index1, false).get()); + regularClient.select(1).get(); + assertEquals("one", regularClient.get(destination).get()); + + // new value for source key + regularClient.select(0).get(); + regularClient.set(source, "two").get(); + + // no REPLACE, copying to existing key on DB 0&1, non-existing key on DB 2 + assertFalse(regularClient.copy(source, destination, index1, false).get()); + assertTrue(regularClient.copy(source, destination, index2, false).get()); + + // new value only gets copied to DB 2 + regularClient.select(1).get(); + assertEquals("one", regularClient.get(destination).get()); + regularClient.select(2).get(); + assertEquals("two", regularClient.get(destination).get()); + + // both exists, with REPLACE, when value isn't the same, source always get copied to + // destination + regularClient.select(0).get(); + assertTrue(regularClient.copy(source, destination, index1, true).get()); + regularClient.select(1).get(); + assertEquals("two", regularClient.get(destination).get()); + } + + // switching back to db 0 + finally { + regularClient.select(0).get(); + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKill_no_write() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionKill_no_write"; + String funcName = "deadlock"; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, true); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + try (var testClient = + GlideClient.createClient(commonClientConfig().requestTimeout(10000).build()).get()) { + try { + // call the function without await + testClient.fcall(funcName); + + Thread.sleep(1000); + + // Run FKILL until it returns OK + boolean functionKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, regularClient.functionKill().get()); + functionKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(functionKilled); + } finally { + waitForNotBusy(regularClient); + } + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKillBinary_no_write() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionKillBinary_no_write"); + GlideString funcName = gs("binary_deadlock"); + GlideString code = + gs(createLuaLibWithLongRunningFunction(libName.toString(), funcName.toString(), 6, true)); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + try (var testClient = + GlideClient.createClient(commonClientConfig().requestTimeout(10000).build()).get()) { + try { + // call the function without await + testClient.fcall(funcName); + + Thread.sleep(1000); + + // Run FKILL until it returns OK + boolean functionKilled = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + assertEquals(OK, regularClient.functionKill().get()); + functionKilled = true; + break; + } catch (RequestException ignored) { + } + Thread.sleep(500); + timeout -= 500; + } + + assertTrue(functionKilled); + } finally { + waitForNotBusy(regularClient); + } + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKill_write_function() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionKill_write_function"; + String funcName = "deadlock_write_function"; + String key = libName; + String code = createLuaLibWithLongRunningFunction(libName, funcName, 6, false); + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + try (var testClient = + GlideClient.createClient(commonClientConfig().requestTimeout(10000).build()).get()) { + try { + // call the function without await + promise = testClient.fcall(funcName, new String[] {key}, new String[0]); + + Thread.sleep(1000); + + boolean foundUnkillable = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + // valkey kills a function with 5 sec delay + // but this will always throw an error in the test + regularClient.functionKill().get(); + } catch (ExecutionException executionException) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if (executionException.getCause() instanceof RequestException + && executionException.getMessage().toLowerCase().contains("unkillable")) { + foundUnkillable = true; + break; + } + } + Thread.sleep(500); + timeout -= 500; + } + assertTrue(foundUnkillable); + + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + // wait for the function to complete (we cannot kill it) + try { + promise.get(); + } catch (Exception ignored) { + } + } + } + } + + @Timeout(20) + @Test + @SneakyThrows + public void functionKillBinary_write_function() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionKill_write_function"); + GlideString funcName = gs("deadlock_write_function"); + GlideString key = libName; + GlideString code = + gs(createLuaLibWithLongRunningFunction(libName.toString(), funcName.toString(), 6, false)); + CompletableFuture promise = new CompletableFuture<>(); + promise.complete(null); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // nothing to kill + var exception = + assertThrows(ExecutionException.class, () -> regularClient.functionKill().get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().toLowerCase().contains("notbusy")); + + // load the lib + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + try (var testClient = + GlideClient.createClient(commonClientConfig().requestTimeout(10000).build()).get()) { + try { + // call the function without await + promise = testClient.fcall(funcName, new GlideString[] {key}, new GlideString[0]); + + Thread.sleep(1000); + + boolean foundUnkillable = false; + int timeout = 4000; // ms + while (timeout >= 0) { + try { + // valkey kills a function with 5 sec delay + // but this will always throw an error in the test + regularClient.functionKill().get(); + } catch (ExecutionException executionException) { + // looking for an error with "unkillable" in the message + // at that point we can break the loop + if (executionException.getCause() instanceof RequestException + && executionException.getMessage().toLowerCase().contains("unkillable")) { + foundUnkillable = true; + break; + } + } + Thread.sleep(500); + timeout -= 500; + } + assertTrue(foundUnkillable); + + } finally { + // If function wasn't killed, and it didn't time out - it blocks the server and cause rest + // test to fail. + // wait for the function to complete (we cannot kill it) + try { + promise.get(); + } catch (Exception ignored) { + } + } + } + } + + @Test + @SneakyThrows + public void functionStats() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + String libName = "functionStats"; + String funcName = libName; + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // function $funcName returns first argument + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), false); + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + var response = regularClient.functionStats().get(); + checkFunctionStatsResponse(response, new String[0], 1, 1); + + code = + generateLuaLibCode( + libName + "_2", + Map.of(funcName + "_2", "return 'OK'", funcName + "_3", "return 42"), + false); + assertEquals(libName + "_2", regularClient.functionLoad(code, true).get()); + + response = regularClient.functionStats().get(); + checkFunctionStatsResponse(response, new String[0], 2, 3); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + response = regularClient.functionStats().get(); + checkFunctionStatsResponse(response, new String[0], 0, 0); + } + + @Test + @SneakyThrows + public void functionStatsBinary() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + GlideString libName = gs("functionStats"); + GlideString funcName = libName; + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // function $funcName returns first argument + GlideString code = + generateLuaLibCodeBinary(libName, Map.of(funcName, gs("return args[1]")), false); + assertEquals(libName, regularClient.functionLoad(code, true).get()); + + var response = regularClient.functionStatsBinary().get(); + checkFunctionStatsBinaryResponse(response, new GlideString[0], 1, 1); + + code = + generateLuaLibCodeBinary( + gs(libName.toString() + "_2"), + Map.of( + gs(funcName.toString() + "_2"), + gs("return 'OK'"), + gs(funcName.toString() + "_3"), + gs("return 42")), + false); + assertEquals(gs(libName.toString() + "_2"), regularClient.functionLoad(code, true).get()); + + response = regularClient.functionStatsBinary().get(); + checkFunctionStatsBinaryResponse(response, new GlideString[0], 2, 3); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + response = regularClient.functionStatsBinary().get(); + checkFunctionStatsBinaryResponse(response, new GlideString[0], 0, 0); + } + + @Test + @SneakyThrows + public void function_dump_and_restore() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in version 7"); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // dumping an empty lib + byte[] emptyDump = regularClient.functionDump().get(); + assertTrue(emptyDump.length > 0); + + String name1 = "Foster"; + String name2 = "Dogster"; + + // function $name1 returns first argument + // function $name2 returns argument array len + String code = + generateLuaLibCode(name1, Map.of(name1, "return args[1]", name2, "return #args"), false); + assertEquals(name1, regularClient.functionLoad(code, true).get()); + var flist = regularClient.functionList(true).get(); + + final byte[] dump = regularClient.functionDump().get(); + + // restore without cleaning the lib and/or overwrite option causes an error + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.functionRestore(dump).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + name1 + " already exists")); + + // APPEND policy also fails for the same reason (name collision) + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionRestore(dump, APPEND).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + name1 + " already exists")); + + // REPLACE policy succeeds + assertEquals(OK, regularClient.functionRestore(dump, REPLACE).get()); + // but nothing changed - all code overwritten + assertDeepEquals(flist, regularClient.functionList(true).get()); + + // create lib with another name, but with the same function names + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + code = generateLuaLibCode(name2, Map.of(name1, "return args[1]", name2, "return #args"), false); + assertEquals(name2, regularClient.functionLoad(code, true).get()); + + // REPLACE policy now fails due to a name collision + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionRestore(dump, REPLACE).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // valkey checks names in random order and blames on first collision + assertTrue( + executionException.getMessage().contains("Function " + name1 + " already exists") + || executionException.getMessage().contains("Function " + name2 + " already exists")); + + // FLUSH policy succeeds, but deletes the second lib + assertEquals(OK, regularClient.functionRestore(dump, FLUSH).get()); + assertDeepEquals(flist, regularClient.functionList(true).get()); + + // call restored functions + assertEquals( + "meow", regularClient.fcall(name1, new String[0], new String[] {"meow", "woem"}).get()); + assertEquals( + 2L, regularClient.fcall(name2, new String[0], new String[] {"meow", "woem"}).get()); + } + + @SneakyThrows + @Test + public void randomkey() { + String key1 = "{key}" + UUID.randomUUID(); + String key2 = "{key}" + UUID.randomUUID(); + + assertEquals(OK, regularClient.set(key1, "a").get()); + assertEquals(OK, regularClient.set(key2, "b").get()); + + String randomKey = regularClient.randomKey().get(); + assertEquals(1L, regularClient.exists(new String[] {randomKey}).get()); + + // no keys in database + assertEquals(OK, regularClient.flushall().get()); + assertNull(regularClient.randomKey().get()); + } + + @SneakyThrows + @Test + public void randomKeyBinary() { + GlideString key1 = gs("{key}" + UUID.randomUUID()); + GlideString key2 = gs("{key}" + UUID.randomUUID()); + + assertEquals(OK, regularClient.set(key1, gs("a")).get()); + assertEquals(OK, regularClient.set(key2, gs("b")).get()); + + GlideString randomKeyBinary = regularClient.randomKeyBinary().get(); + assertEquals(1L, regularClient.exists(new GlideString[] {randomKeyBinary}).get()); + + // no keys in database + assertEquals(OK, regularClient.flushall().get()); + assertNull(regularClient.randomKeyBinary().get()); + } + + @Test + @SneakyThrows + public void sort() { + String setKey1 = "setKey1"; + String setKey2 = "setKey2"; + String setKey3 = "setKey3"; + String setKey4 = "setKey4"; + String setKey5 = "setKey5"; + String[] setKeys = new String[] {setKey1, setKey2, setKey3, setKey4, setKey5}; + String listKey = "listKey"; + String storeKey = "storeKey"; + String nameField = "name"; + String ageField = "age"; + String[] names = new String[] {"Alice", "Bob", "Charlie", "Dave", "Eve"}; + String[] namesSortedByAge = new String[] {"Dave", "Bob", "Alice", "Charlie", "Eve"}; + String[] ages = new String[] {"30", "25", "35", "20", "40"}; + String[] userIDs = new String[] {"3", "1", "5", "4", "2"}; + String namePattern = "setKey*->name"; + String agePattern = "setKey*->age"; + String missingListKey = "100000"; + + for (int i = 0; i < setKeys.length; i++) { + assertEquals( + 2, regularClient.hset(setKeys[i], Map.of(nameField, names[i], ageField, ages[i])).get()); + } + + assertEquals(5, regularClient.rpush(listKey, userIDs).get()); + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sort( + listKey, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient.sort(listKey, SortOptions.builder().byPattern("noSort").build()).get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sort(listKey, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sort( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + + // SORT_RO + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sortReadOnly(listKey, SortOptions.builder().byPattern("noSort").build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sortReadOnly(listKey, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + assertArrayEquals( + namesSortedByAge, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + } + + // SORT with STORE + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder() + .limit(new Limit(0L, -1L)) + .orderBy(ASC) + .byPattern(agePattern) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + } + + @Test + @SneakyThrows + public void sort_binary() { + GlideString setKey1 = gs("setKey1"); + GlideString setKey2 = gs("setKey2"); + GlideString setKey3 = gs("setKey3"); + GlideString setKey4 = gs("setKey4"); + GlideString setKey5 = gs("setKey5"); + GlideString[] setKeys = new GlideString[] {setKey1, setKey2, setKey3, setKey4, setKey5}; + GlideString listKey = gs("listKey"); + GlideString storeKey = gs("storeKey"); + GlideString nameField = gs("name"); + GlideString ageField = gs("age"); + GlideString[] names = + new GlideString[] {gs("Alice"), gs("Bob"), gs("Charlie"), gs("Dave"), gs("Eve")}; + String[] namesSortedByAge = new String[] {"Dave", "Bob", "Alice", "Charlie", "Eve"}; + GlideString[] namesSortedByAge_gs = + new GlideString[] {gs("Dave"), gs("Bob"), gs("Alice"), gs("Charlie"), gs("Eve")}; + GlideString[] ages = new GlideString[] {gs("30"), gs("25"), gs("35"), gs("20"), gs("40")}; + GlideString[] userIDs = new GlideString[] {gs("3"), gs("1"), gs("5"), gs("4"), gs("2")}; + GlideString namePattern = gs("setKey*->name"); + GlideString agePattern = gs("setKey*->age"); + GlideString missingListKey = gs("100000"); + + for (int i = 0; i < setKeys.length; i++) { + assertEquals( + 2, + regularClient + .hset( + setKeys[i].toString(), + Map.of( + nameField.toString(), + names[i].toString(), + ageField.toString(), + ages[i].toString())) + .get()); + } + + assertEquals(5, regularClient.rpush(listKey, userIDs).get()); + assertArrayEquals( + new GlideString[] {gs("Alice"), gs("Bob")}, + regularClient + .sort( + listKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, 2L)) + .getPattern(namePattern) + .build()) + .get()); + + assertArrayEquals( + new GlideString[] {gs("Eve"), gs("Dave")}, + regularClient + .sort( + listKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new GlideString[] {gs("Eve"), gs("40"), gs("Charlie"), gs("35")}, + regularClient + .sort( + listKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sort(listKey, SortOptionsBinary.builder().byPattern(gs("noSort")).build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new GlideString[] {null, null, null, null, null}, + regularClient + .sort(listKey, SortOptionsBinary.builder().alpha().getPattern(gs("missing")).build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new GlideString[] {missingListKey}).get()); + assertArrayEquals( + new GlideString[] {null, gs("Dave"), gs("Bob"), gs("Alice"), gs("Charlie"), gs("Eve")}, + regularClient + .sort( + listKey, + SortOptionsBinary.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey.toString(), regularClient.lpop(listKey.toString()).get()); + + // SORT_RO + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + new GlideString[] {gs("Alice"), gs("Bob")}, + regularClient + .sortReadOnly( + listKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, 2L)) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new GlideString[] {gs("Eve"), gs("Dave")}, + regularClient + .sortReadOnly( + listKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new GlideString[] {gs("Eve"), gs("40"), gs("Charlie"), gs("35")}, + regularClient + .sortReadOnly( + listKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sortReadOnly(listKey, SortOptionsBinary.builder().byPattern(gs("noSort")).build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new GlideString[] {null, null, null, null, null}, + regularClient + .sortReadOnly( + listKey, SortOptionsBinary.builder().alpha().getPattern(gs("missing")).build()) + .get()); + + assertArrayEquals( + namesSortedByAge_gs, + regularClient + .sortReadOnly( + listKey, + SortOptionsBinary.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new GlideString[] {missingListKey}).get()); + assertArrayEquals( + new GlideString[] {null, gs("Dave"), gs("Bob"), gs("Alice"), gs("Charlie"), gs("Eve")}, + regularClient + .sortReadOnly( + listKey, + SortOptionsBinary.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey.toString(), regularClient.lpop(listKey.toString()).get()); + } + + // SORT with STORE + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptionsBinary.builder() + .limit(new Limit(0L, -1L)) + .orderBy(ASC) + .byPattern(agePattern) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey.toString(), 0, -1).get()); + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptionsBinary.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey.toString(), 0, -1).get()); + } + + @Test + @SneakyThrows + public void scan() { + String initialCursor = "0"; + + int numberKeys = 500; + Map keys = new HashMap<>(); + for (int i = 0; i < numberKeys; i++) { + keys.put("{key}-" + i + "-" + UUID.randomUUID(), "{value}-" + i + "-" + UUID.randomUUID()); + } + + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // empty the database + assertEquals(OK, regularClient.flushdb().get()); + + // Empty return + Object[] emptyResult = regularClient.scan(initialCursor).get(); + assertEquals(initialCursor, emptyResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); + + // Negative cursor + Object[] negativeResult = regularClient.scan("-1").get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + + // Add keys to the database using mset + regularClient.mset(keys).get(); + + Object[] result; + Object[] keysFound = new String[0]; + String resultCursor = "0"; + boolean isFirstLoop = true; + do { + result = regularClient.scan(resultCursor).get(); + resultCursor = result[resultCursorIndex].toString(); + Object[] resultKeys = (Object[]) result[resultCollectionIndex]; + keysFound = ArrayUtils.addAll(keysFound, resultKeys); + + if (isFirstLoop) { + assertNotEquals("0", resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals("0")) { + break; + } + } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + // check that each key added to the database is found through the cursor + Object[] finalKeysFound = keysFound; + keys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key))); + } + + @Test + @SneakyThrows + public void scan_binary() { + GlideString initialCursor = gs("0"); + + int numberKeys = 500; + Map keys = new HashMap<>(); + for (int i = 0; i < numberKeys; i++) { + keys.put( + gs("{key}-" + i + "-" + UUID.randomUUID()), gs("{value}-" + i + "-" + UUID.randomUUID())); + } + + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // empty the database + assertEquals(OK, regularClient.flushdb().get()); + + // Empty return + Object[] emptyResult = regularClient.scan(initialCursor).get(); + assertEquals(initialCursor, emptyResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); + + // Negative cursor + Object[] negativeResult = regularClient.scan(gs("-1")).get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + + // Add keys to the database using mset + regularClient.msetBinary(keys).get(); + + Object[] result; + Object[] keysFound = new GlideString[0]; + GlideString resultCursor = gs("0"); + boolean isFirstLoop = true; + do { + result = regularClient.scan(resultCursor).get(); + resultCursor = (GlideString) result[resultCursorIndex]; + Object[] resultKeys = (Object[]) result[resultCollectionIndex]; + keysFound = ArrayUtils.addAll(keysFound, resultKeys); + + if (isFirstLoop) { + assertNotEquals(gs("0"), resultCursor); + isFirstLoop = false; + } else if (resultCursor.equals(gs("0"))) { + break; + } + } while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. + + // check that each key added to the database is found through the cursor + Object[] finalKeysFound = keysFound; + keys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key))); + } + + @Test + @SneakyThrows + public void scan_with_options() { + String initialCursor = "0"; + String matchPattern = UUID.randomUUID().toString(); + + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Add string keys to the database using mset + Map stringKeys = new HashMap<>(); + for (int i = 0; i < 10; i++) { + stringKeys.put("{key}-" + i + "-" + matchPattern, "{value}-" + i + "-" + matchPattern); + } + regularClient.mset(stringKeys).get(); + + // Add set keys to the database using sadd + List setKeys = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + String key = "{key}-set-" + i + "-" + matchPattern; + regularClient.sadd( + gs(key), + new GlideString[] {gs(UUID.randomUUID().toString()), gs(UUID.randomUUID().toString())}); + setKeys.add(key); + } + + // Empty return - match a random UUID + ScanOptions options = ScanOptions.builder().matchPattern("*" + UUID.randomUUID()).build(); + Object[] emptyResult = regularClient.scan(initialCursor, options).get(); + assertNotEquals(initialCursor, emptyResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); + + // Negative cursor + Object[] negativeResult = regularClient.scan("-1", options).get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + + // scan for strings by match pattern: + options = + ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(STRING).build(); + Object[] result; + Object[] keysFound = new String[0]; + String resultCursor = "0"; + do { + result = regularClient.scan(resultCursor, options).get(); + resultCursor = result[resultCursorIndex].toString(); + Object[] resultKeys = (Object[]) result[resultCollectionIndex]; + keysFound = ArrayUtils.addAll(keysFound, resultKeys); + } while (!resultCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + // check that each key added to the database is found through the cursor + Object[] finalKeysFound = keysFound; + stringKeys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key))); + + // scan for sets by match pattern: + options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(SET).build(); + Object[] setResult; + Object[] setsFound = new String[0]; + String setCursor = "0"; + do { + setResult = regularClient.scan(setCursor, options).get(); + setCursor = setResult[resultCursorIndex].toString(); + Object[] resultKeys = (Object[]) setResult[resultCollectionIndex]; + setsFound = ArrayUtils.addAll(setsFound, resultKeys); + } while (!setCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + + // check that each key added to the database is found through the cursor + Object[] finalSetsFound = setsFound; + setKeys.forEach(k -> assertTrue(ArrayUtils.contains(finalSetsFound, k))); + + // scan for hashes by match pattern: + // except in this case, we should never find anything + options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(HASH).build(); + String hashCursor = "0"; + do { + Object[] hashResult = regularClient.scan(hashCursor, options).get(); + hashCursor = hashResult[resultCursorIndex].toString(); + assertEquals(0, ((Object[]) hashResult[resultCollectionIndex]).length); + } while (!hashCursor.equals("0")); // 0 is returned for the cursor of the last iteration. + } + + @Test + @SneakyThrows + public void scan_binary_with_options() { + GlideString initialCursor = gs("0"); + String matchPattern = UUID.randomUUID().toString(); + + int resultCursorIndex = 0; + int resultCollectionIndex = 1; + + // Add string keys to the database using mset + Map stringKeys = new HashMap<>(); + for (int i = 0; i < 10; i++) { + stringKeys.put( + gs("{key}-" + i + "-" + matchPattern), gs("{value}-" + i + "-" + matchPattern)); + } + regularClient.msetBinary(stringKeys).get(); + + // Add set keys to the database using sadd + List setKeys = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + GlideString key = gs("{key}-set-" + i + "-" + matchPattern); + regularClient.sadd( + key, + new GlideString[] {gs(UUID.randomUUID().toString()), gs(UUID.randomUUID().toString())}); + setKeys.add(key); + } + + // Empty return - match a random UUID + ScanOptions options = ScanOptions.builder().matchPattern("*" + UUID.randomUUID()).build(); + Object[] emptyResult = regularClient.scan(initialCursor, options).get(); + assertNotEquals(initialCursor, emptyResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, emptyResult[resultCollectionIndex]); + + // Negative cursor + Object[] negativeResult = regularClient.scan(gs("-1"), options).get(); + assertEquals(initialCursor, negativeResult[resultCursorIndex]); + assertDeepEquals(new String[] {}, negativeResult[resultCollectionIndex]); + + // scan for strings by match pattern: + options = + ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(STRING).build(); + Object[] result; + Object[] keysFound = new GlideString[0]; + GlideString resultCursor = gs("0"); + do { + result = regularClient.scan(resultCursor, options).get(); + resultCursor = (GlideString) result[resultCursorIndex]; + Object[] resultKeys = (Object[]) result[resultCollectionIndex]; + keysFound = ArrayUtils.addAll(keysFound, resultKeys); + } while (!resultCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. + + // check that each key added to the database is found through the cursor + Object[] finalKeysFound = keysFound; + stringKeys.forEach((key, value) -> assertTrue(ArrayUtils.contains(finalKeysFound, key))); + + // scan for sets by match pattern: + options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(SET).build(); + Object[] setResult; + Object[] setsFound = new GlideString[0]; + GlideString setCursor = gs("0"); + do { + setResult = regularClient.scan(setCursor, options).get(); + setCursor = (GlideString) setResult[resultCursorIndex]; + Object[] resultKeys = (Object[]) setResult[resultCollectionIndex]; + setsFound = ArrayUtils.addAll(setsFound, resultKeys); + } while (!setCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. + + // check that each key added to the database is found through the cursor + Object[] finalSetsFound = setsFound; + setKeys.forEach(k -> assertTrue(ArrayUtils.contains(finalSetsFound, k))); + + // scan for hashes by match pattern: + // except in this case, we should never find anything + options = ScanOptions.builder().matchPattern("*" + matchPattern).count(100L).type(HASH).build(); + GlideString hashCursor = gs("0"); + do { + Object[] hashResult = regularClient.scan(hashCursor, options).get(); + hashCursor = (GlideString) hashResult[resultCursorIndex]; + assertEquals(0, ((Object[]) hashResult[resultCollectionIndex]).length); + } while (!hashCursor.equals(gs("0"))); // 0 is returned for the cursor of the last iteration. + } } diff --git a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java index 3f36952049..e61b97ed4e 100644 --- a/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java +++ b/java/integTest/src/test/java/glide/standalone/StandaloneClientTests.java @@ -1,7 +1,7 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.standalone; -import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestConfiguration.SERVER_VERSION; import static glide.TestUtilities.commonClientConfig; import static glide.TestUtilities.getRandomString; import static glide.api.BaseClient.OK; @@ -10,8 +10,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import glide.api.RedisClient; -import glide.api.models.configuration.RedisCredentials; +import glide.api.GlideClient; +import glide.api.models.configuration.ServerCredentials; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.RequestException; import java.util.concurrent.ExecutionException; @@ -27,10 +27,10 @@ public class StandaloneClientTests { public void register_client_name_and_version() { String minVersion = "7.2.0"; assumeTrue( - REDIS_VERSION.isGreaterThanOrEqualTo(minVersion), - "Redis version required >= " + minVersion); + SERVER_VERSION.isGreaterThanOrEqualTo(minVersion), + "Valkey version required >= " + minVersion); - RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get(); + GlideClient client = GlideClient.createClient(commonClientConfig().build()).get(); String info = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get(); assertTrue(info.contains("lib-name=GlideJava")); @@ -42,7 +42,7 @@ public void register_client_name_and_version() { @SneakyThrows @Test public void can_connect_with_auth_require_pass() { - RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get(); + GlideClient client = GlideClient.createClient(commonClientConfig().build()).get(); String password = "TEST_AUTH"; client.customCommand(new String[] {"CONFIG", "SET", "requirepass", password}).get(); @@ -51,14 +51,14 @@ public void can_connect_with_auth_require_pass() { ExecutionException exception = assertThrows( ExecutionException.class, - () -> RedisClient.CreateClient(commonClientConfig().build()).get()); + () -> GlideClient.createClient(commonClientConfig().build()).get()); assertTrue(exception.getCause() instanceof ClosingException); // Creation of a new client with credentials - RedisClient auth_client = - RedisClient.CreateClient( + GlideClient auth_client = + GlideClient.createClient( commonClientConfig() - .credentials(RedisCredentials.builder().password(password).build()) + .credentials(ServerCredentials.builder().password(password).build()) .build()) .get(); @@ -78,7 +78,7 @@ public void can_connect_with_auth_require_pass() { @SneakyThrows @Test public void can_connect_with_auth_acl() { - RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get(); + GlideClient client = GlideClient.createClient(commonClientConfig().build()).get(); String username = "testuser"; String password = "TEST_AUTH"; @@ -107,11 +107,11 @@ public void can_connect_with_auth_acl() { assertEquals(OK, client.set(key, value).get()); // Creation of a new client with credentials - RedisClient testUserClient = - RedisClient.CreateClient( + GlideClient testUserClient = + GlideClient.createClient( commonClientConfig() .credentials( - RedisCredentials.builder().username(username).password(password).build()) + ServerCredentials.builder().username(username).password(password).build()) .build()) .get(); @@ -129,7 +129,7 @@ public void can_connect_with_auth_acl() { @SneakyThrows @Test public void select_standalone_database_id() { - RedisClient client = RedisClient.CreateClient(commonClientConfig().databaseId(4).build()).get(); + GlideClient client = GlideClient.createClient(commonClientConfig().databaseId(4).build()).get(); String clientInfo = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get(); assertTrue(clientInfo.contains("db=4")); @@ -140,8 +140,8 @@ public void select_standalone_database_id() { @SneakyThrows @Test public void client_name() { - RedisClient client = - RedisClient.CreateClient(commonClientConfig().clientName("TEST_CLIENT_NAME").build()).get(); + GlideClient client = + GlideClient.createClient(commonClientConfig().clientName("TEST_CLIENT_NAME").build()).get(); String clientInfo = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get(); assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME")); @@ -152,7 +152,7 @@ public void client_name() { @Test @SneakyThrows public void closed_client_throws_ExecutionException_with_ClosingException_as_cause() { - RedisClient client = RedisClient.CreateClient(commonClientConfig().build()).get(); + GlideClient client = GlideClient.createClient(commonClientConfig().build()).get(); client.close(); ExecutionException executionException = diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index 532adb7703..e78f027c29 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -1,45 +1,63 @@ -/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.standalone; -import static glide.TransactionTestUtilities.transactionTest; -import static glide.TransactionTestUtilities.transactionTestResult; +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestUtilities.assertDeepEquals; +import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.generateLuaLibCode; import static glide.api.BaseClient.OK; +import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.HASH; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.LIST; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.SET; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.STREAM; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.STRING; +import static glide.api.models.commands.scan.ScanOptions.ObjectType.ZSET; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; -import glide.TestConfiguration; -import glide.api.RedisClient; +import glide.TransactionTestUtilities.TransactionBuilder; +import glide.api.GlideClient; +import glide.api.models.GlideString; import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; -import glide.api.models.configuration.NodeAddress; -import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; +import glide.api.models.commands.scan.ScanOptions; +import glide.api.models.commands.stream.StreamAddOptions; +import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; @Timeout(10) // seconds public class TransactionTests { - private static RedisClient client = null; + private static GlideClient client = null; @BeforeAll @SneakyThrows public static void init() { - client = - RedisClient.CreateClient( - RedisClientConfiguration.builder() - .address( - NodeAddress.builder().port(TestConfiguration.STANDALONE_PORTS[0]).build()) - .build()) - .get(); + client = GlideClient.createClient(commonClientConfig().requestTimeout(7000).build()).get(); } @AfterAll @@ -92,22 +110,73 @@ public void ping_tests() { } } + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("glide.TransactionTestUtilities#getCommonTransactionBuilders") + public void transactions_with_group_of_commands(String testName, TransactionBuilder builder) { + Transaction transaction = new Transaction(); + Object[] expectedResult = builder.apply(transaction); + + Object[] results = client.exec(transaction).get(); + assertDeepEquals(expectedResult, results); + } + + @SneakyThrows + @ParameterizedTest(name = "{0}") + @MethodSource("glide.TransactionTestUtilities#getPrimaryNodeTransactionBuilders") + public void keyless_transactions_with_group_of_commands( + String testName, TransactionBuilder builder) { + Transaction transaction = new Transaction(); + Object[] expectedResult = builder.apply(transaction); + + Object[] results = client.exec(transaction).get(); + assertDeepEquals(expectedResult, results); + } + @SneakyThrows @Test - public void test_standalone_transactions() { - Transaction transaction = (Transaction) transactionTest(new Transaction()); - Object[] expectedResult = transactionTestResult(); + public void test_transaction_large_values() { + int length = 1 << 25; // 33mb + String key = "0".repeat(length); + String value = "0".repeat(length); + + Transaction transaction = new Transaction(); + transaction.set(key, value); + transaction.get(key); + Object[] expectedResult = + new Object[] { + OK, // transaction.set(key, value); + value, // transaction.get(key); + }; + + Object[] result = client.exec(transaction).get(); + assertArrayEquals(expectedResult, result); + } + + @SneakyThrows + @Test + public void test_standalone_transaction() { String key = UUID.randomUUID().toString(); String value = UUID.randomUUID().toString(); - transaction.select(1); + Transaction transaction = new Transaction(); transaction.set(key, value); transaction.get(key); - transaction.select(0); + transaction.move(key, 1L); + transaction.get(key); + transaction.select(1); transaction.get(key); - expectedResult = ArrayUtils.addAll(expectedResult, OK, OK, value, OK, null); + Object[] expectedResult = + new Object[] { + OK, // transaction.set(key, value); + value, // transaction.get(key); + true, // transaction.move(key, 1L); + null, // transaction.get(key); + OK, // transaction.select(1); + value // transaction.get(key); + }; Object[] result = client.exec(transaction).get(); assertArrayEquals(expectedResult, result); @@ -121,4 +190,621 @@ public void lastsave() { var response = client.exec(new Transaction().lastsave()).get(); assertTrue(Instant.ofEpochSecond((long) response[0]).isAfter(yesterday)); } + + @Test + @SneakyThrows + public void objectFreq() { + String objectFreqKey = "key"; + String maxmemoryPolicy = "maxmemory-policy"; + + String oldPolicy = client.configGet(new String[] {maxmemoryPolicy}).get().get(maxmemoryPolicy); + try { + Transaction transaction = new Transaction(); + transaction.configSet(Map.of(maxmemoryPolicy, "allkeys-lfu")); + transaction.set(objectFreqKey, ""); + transaction.objectFreq(objectFreqKey); + var response = client.exec(transaction).get(); + assertEquals(OK, response[0]); + assertEquals(OK, response[1]); + assertTrue((long) response[2] >= 0L); + } finally { + client.configSet(Map.of(maxmemoryPolicy, oldPolicy)).get(); + } + } + + @Test + @SneakyThrows + public void objectIdletime() { + String objectIdletimeKey = "key"; + Transaction transaction = new Transaction(); + transaction.set(objectIdletimeKey, ""); + transaction.objectIdletime(objectIdletimeKey); + var response = client.exec(transaction).get(); + assertEquals(OK, response[0]); + assertTrue((long) response[1] >= 0L); + } + + @Test + @SneakyThrows + public void objectRefcount() { + String objectRefcountKey = "key"; + Transaction transaction = new Transaction(); + transaction.set(objectRefcountKey, ""); + transaction.objectRefcount(objectRefcountKey); + var response = client.exec(transaction).get(); + assertEquals(OK, response[0]); + assertTrue((long) response[1] >= 0L); + } + + @Test + @SneakyThrows + public void zrank_zrevrank_withscores() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.2.0")); + String zSetKey1 = "{key}:zsetKey1-" + UUID.randomUUID(); + Transaction transaction = new Transaction(); + transaction.zadd(zSetKey1, Map.of("one", 1.0, "two", 2.0, "three", 3.0)); + transaction.zrankWithScore(zSetKey1, "one"); + transaction.zrevrankWithScore(zSetKey1, "one"); + + Object[] result = client.exec(transaction).get(); + assertEquals(3L, result[0]); + assertArrayEquals(new Object[] {0L, 1.0}, (Object[]) result[1]); + assertArrayEquals(new Object[] {2L, 1.0}, (Object[]) result[2]); + } + + @Test + @SneakyThrows + public void copy() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("6.2.0")); + // setup + String copyKey1 = "{CopyKey}-1-" + UUID.randomUUID(); + String copyKey2 = "{CopyKey}-2-" + UUID.randomUUID(); + Transaction transaction = + new Transaction() + .copy(copyKey1, copyKey2, 1, false) + .set(copyKey1, "one") + .set(copyKey2, "two") + .copy(copyKey1, copyKey2, 1, false) + .copy(copyKey1, copyKey2, 1, true) + .copy(copyKey1, copyKey2, 2, true) + .select(1) + .get(copyKey2) + .select(2) + .get(copyKey2); + Object[] expectedResult = + new Object[] { + false, // copy(copyKey1, copyKey2, 1, false) + OK, // set(copyKey1, "one") + OK, // set(copyKey2, "two") + true, // copy(copyKey1, copyKey2, 1, false) + true, // copy(copyKey1, copyKey2, 1, true) + true, // copy(copyKey1, copyKey2, 2, true) + OK, // select(1) + "one", // get(copyKey2) + OK, // select(2) + "one", // get(copyKey2) + }; + + Object[] result = client.exec(transaction).get(); + assertArrayEquals(expectedResult, result); + } + + @Test + @SneakyThrows + public void watch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String key4 = "{key}-4" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2, key3}; + Transaction setFoobarTransaction = new Transaction(); + Transaction setHelloTransaction = new Transaction(); + String[] expectedExecResponse = new String[] {OK, OK, OK}; + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + assertEquals(OK, client.watch(keys).get()); + assertEquals(OK, client.set(key2, helloString).get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString).set(key3, foobarString); + assertNull(client.exec(setFoobarTransaction).get()); + assertNull(client.get(key1).get()); // Sanity check + assertEquals(helloString, client.get(key2).get()); + assertNull(client.get(key3).get()); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + assertEquals(OK, client.watch(keys).get()); + assertEquals(helloString, client.get(key2).get()); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(foobarString, client.get(key1).get()); // Sanity check + assertEquals(foobarString, client.get(key2).get()); + assertEquals(foobarString, client.get(key3).get()); + + // Transaction executes command successfully with unmodified watched keys + assertEquals(OK, client.watch(keys).get()); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(foobarString, client.get(key1).get()); // Sanity check + assertEquals(foobarString, client.get(key2).get()); + assertEquals(foobarString, client.get(key3).get()); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + assertEquals(OK, client.watch(new String[] {key4}).get()); + setHelloTransaction.set(key1, helloString).set(key2, helloString).set(key3, helloString); + assertArrayEquals(expectedExecResponse, client.exec(setHelloTransaction).get()); + assertEquals(helloString, client.get(key1).get()); // Sanity check + assertEquals(helloString, client.get(key2).get()); + assertEquals(helloString, client.get(key3).get()); + + // WATCH can not have an empty String array parameter + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.watch(new String[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @Test + @SneakyThrows + public void watch_binary() { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + GlideString key3 = gs("{key}-3" + UUID.randomUUID()); + GlideString key4 = gs("{key}-4" + UUID.randomUUID()); + String foobarString = "foobar"; + String helloString = "hello"; + GlideString[] keys = new GlideString[] {key1, key2, key3}; + Transaction setFoobarTransaction = new Transaction(); + Transaction setHelloTransaction = new Transaction(); + String[] expectedExecResponse = new String[] {OK, OK, OK}; + + // Returns null when a watched key is modified before it is executed in a transaction command. + // Transaction commands are not performed. + assertEquals(OK, client.watch(keys).get()); + assertEquals(OK, client.set(key2, gs(helloString)).get()); + setFoobarTransaction + .set(key1.toString(), foobarString) + .set(key2.toString(), foobarString) + .set(key3.toString(), foobarString); + assertNull(client.exec(setFoobarTransaction).get()); + assertNull(client.get(key1).get()); // Sanity check + assertEquals(gs(helloString), client.get(key2).get()); + assertNull(client.get(key3).get()); + + // Transaction executes command successfully with a read command on the watch key before + // transaction is executed. + assertEquals(OK, client.watch(keys).get()); + assertEquals(gs(helloString), client.get(key2).get()); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(gs(foobarString), client.get(key1).get()); // Sanity check + assertEquals(gs(foobarString), client.get(key2).get()); + assertEquals(gs(foobarString), client.get(key3).get()); + + // Transaction executes command successfully with unmodified watched keys + assertEquals(OK, client.watch(keys).get()); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(gs(foobarString), client.get(key1).get()); // Sanity check + assertEquals(gs(foobarString), client.get(key2).get()); + assertEquals(gs(foobarString), client.get(key3).get()); + + // Transaction executes command successfully with a modified watched key but is not in the + // transaction. + assertEquals(OK, client.watch(new GlideString[] {key4}).get()); + setHelloTransaction + .set(key1.toString(), helloString) + .set(key2.toString(), helloString) + .set(key3.toString(), helloString); + assertArrayEquals(expectedExecResponse, client.exec(setHelloTransaction).get()); + assertEquals(gs(helloString), client.get(key1).get()); // Sanity check + assertEquals(gs(helloString), client.get(key2).get()); + assertEquals(gs(helloString), client.get(key3).get()); + + // WATCH can not have an empty String array parameter + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.watch(new GlideString[] {}).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @Test + @SneakyThrows + public void unwatch() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String foobarString = "foobar"; + String helloString = "hello"; + String[] keys = new String[] {key1, key2}; + Transaction setFoobarTransaction = new Transaction(); + String[] expectedExecResponse = new String[] {OK, OK}; + + // UNWATCH returns OK when there no watched keys + assertEquals(OK, client.unwatch().get()); + + // Transaction executes successfully after modifying a watched key then calling UNWATCH + assertEquals(OK, client.watch(keys).get()); + assertEquals(OK, client.set(key2, helloString).get()); + assertEquals(OK, client.unwatch().get()); + setFoobarTransaction.set(key1, foobarString).set(key2, foobarString); + assertArrayEquals(expectedExecResponse, client.exec(setFoobarTransaction).get()); + assertEquals(foobarString, client.get(key1).get()); + assertEquals(foobarString, client.get(key2).get()); + } + + @Test + @SneakyThrows + public void sort_and_sortReadOnly() { + Transaction transaction1 = new Transaction(); + Transaction transaction2 = new Transaction(); + String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); + String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + String[] ascendingListByAge = new String[] {"Bob", "Alice"}; + String[] descendingListByAge = new String[] {"Alice", "Bob"}; + + transaction1 + .hset("user:1", Map.of("name", "Alice", "age", "30")) + .hset("user:2", Map.of("name", "Bob", "age", "25")) + .lpush(genericKey1, new String[] {"2", "1"}) + .sort( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sort( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .lrange(genericKey2, 0, -1) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .lrange(genericKey2, 0, -1); + + var expectedResults = + new Object[] { + 2L, // hset("user:1", Map.of("name", "Alice", "age", "30")) + 2L, // hset("user:2", Map.of("name", "Bob", "age", "25")) + 2L, // lpush(genericKey1, new String[] {"2", "1"}) + ascendingListByAge, // sort(genericKey1, SortOptions) + descendingListByAge, // sort(genericKey1, SortOptions) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + ascendingListByAge, // lrange(genericKey4, 0, -1) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + descendingListByAge, // lrange(genericKey2, 0, -1) + }; + + assertArrayEquals(expectedResults, client.exec(transaction1).get()); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction2 + .sortReadOnly( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sortReadOnly( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()); + + expectedResults = + new Object[] { + ascendingListByAge, // sortReadOnly(genericKey1, SortOptions) + descendingListByAge, // sortReadOnly(genericKey1, SortOptions) + }; + + assertArrayEquals(expectedResults, client.exec(transaction2).get()); + } + } + + @SneakyThrows + @Test + public void waitTest() { + // setup + String key = UUID.randomUUID().toString(); + long numreplicas = 1L; + long timeout = 1000L; + Transaction transaction = new Transaction(); + + transaction.set(key, "value"); + transaction.wait(numreplicas, timeout); + + Object[] results = client.exec(transaction).get(); + Object[] expectedResult = + new Object[] { + OK, // set(key, "value") + 0L, // wait(numreplicas, timeout) + }; + assertEquals(expectedResult[0], results[0]); + assertTrue((long) expectedResult[1] <= (long) results[1]); + } + + @SneakyThrows + @Test + public void scan_test() { + // setup + String key = UUID.randomUUID().toString(); + Map msetMap = Map.of(key, UUID.randomUUID().toString()); + assertEquals(OK, client.mset(msetMap).get()); + + String cursor = "0"; + Object[] keysFound = new Object[0]; + do { + Transaction transaction = new Transaction(); + transaction.scan(cursor); + Object[] results = client.exec(transaction).get(); + cursor = (String) ((Object[]) results[0])[0]; + keysFound = ArrayUtils.addAll(keysFound, (Object[]) ((Object[]) results[0])[1]); + } while (!cursor.equals("0")); + + assertTrue(ArrayUtils.contains(keysFound, key)); + } + + @SneakyThrows + @Test + public void scan_binary_test() { + // setup + String key = UUID.randomUUID().toString(); + Map msetMap = Map.of(key, UUID.randomUUID().toString()); + assertEquals(OK, client.mset(msetMap).get()); + + GlideString cursor = gs("0"); + Object[] keysFound = new Object[0]; + do { + Transaction transaction = new Transaction().withBinaryOutput().scan(cursor); + Object[] results = client.exec(transaction).get(); + cursor = (GlideString) ((Object[]) results[0])[0]; + keysFound = ArrayUtils.addAll(keysFound, (Object[]) ((Object[]) results[0])[1]); + } while (!cursor.equals(gs("0"))); + + assertTrue(ArrayUtils.contains(keysFound, gs(key))); + } + + @SneakyThrows + @Test + public void scan_with_options_test() { + // setup + Transaction setupTransaction = new Transaction(); + + Map typeKeys = + Map.of( + STRING, "{string}-" + UUID.randomUUID(), + LIST, "{list}-" + UUID.randomUUID(), + SET, "{set}-" + UUID.randomUUID(), + ZSET, "{zset}-" + UUID.randomUUID(), + HASH, "{hash}-" + UUID.randomUUID(), + STREAM, "{stream}-" + UUID.randomUUID()); + + setupTransaction.set(typeKeys.get(STRING), UUID.randomUUID().toString()); + setupTransaction.lpush(typeKeys.get(LIST), new String[] {UUID.randomUUID().toString()}); + setupTransaction.sadd(typeKeys.get(SET), new String[] {UUID.randomUUID().toString()}); + setupTransaction.zadd(typeKeys.get(ZSET), Map.of(UUID.randomUUID().toString(), 1.0)); + setupTransaction.hset( + typeKeys.get(HASH), Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + setupTransaction.xadd( + typeKeys.get(STREAM), Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + assertNotNull(client.exec(setupTransaction).get()); + + for (var type : ScanOptions.ObjectType.values()) { + ScanOptions options = ScanOptions.builder().type(type).count(99L).build(); + + String cursor = "0"; + Object[] keysFound = new Object[0]; + do { + Transaction transaction = new Transaction(); + transaction.scan(cursor, options); + Object[] results = client.exec(transaction).get(); + cursor = (String) ((Object[]) results[0])[0]; + keysFound = ArrayUtils.addAll(keysFound, (Object[]) ((Object[]) results[0])[1]); + } while (!cursor.equals("0")); + + assertTrue( + ArrayUtils.contains(keysFound, typeKeys.get(type)), + "Unable to find " + typeKeys.get(type) + " in a scan by type"); + + options = ScanOptions.builder().matchPattern(typeKeys.get(type)).count(42L).build(); + cursor = "0"; + keysFound = new Object[0]; + do { + Transaction transaction = new Transaction(); + transaction.scan(cursor, options); + Object[] results = client.exec(transaction).get(); + cursor = (String) ((Object[]) results[0])[0]; + keysFound = ArrayUtils.addAll(keysFound, (Object[]) ((Object[]) results[0])[1]); + } while (!cursor.equals("0")); + + assertTrue( + ArrayUtils.contains(keysFound, typeKeys.get(type)), + "Unable to find " + typeKeys.get(type) + " in a scan by match pattern"); + } + } + + @SneakyThrows + @Test + public void scan_binary_with_options_test() { + // setup + Transaction setupTransaction = new Transaction().withBinaryOutput(); + + Map typeKeys = + Map.of( + STRING, gs("{string}-" + UUID.randomUUID()), + LIST, gs("{list}-" + UUID.randomUUID()), + SET, gs("{set}-" + UUID.randomUUID()), + ZSET, gs("{zset}-" + UUID.randomUUID()), + HASH, gs("{hash}-" + UUID.randomUUID()), + STREAM, gs("{stream}-" + UUID.randomUUID())); + + setupTransaction.set(typeKeys.get(STRING), UUID.randomUUID().toString()); + setupTransaction.lpush(typeKeys.get(LIST), new String[] {UUID.randomUUID().toString()}); + setupTransaction.sadd(typeKeys.get(SET), new String[] {UUID.randomUUID().toString()}); + setupTransaction.zadd(typeKeys.get(ZSET), Map.of(UUID.randomUUID().toString(), 1.0)); + setupTransaction.hset( + typeKeys.get(HASH), Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + setupTransaction.xadd( + typeKeys.get(STREAM), Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + assertNotNull(client.exec(setupTransaction).get()); + + final GlideString initialCursor = gs("0"); + + for (var type : ScanOptions.ObjectType.values()) { + ScanOptions options = ScanOptions.builder().type(type).count(99L).build(); + + GlideString cursor = initialCursor; + Object[] keysFound = new Object[0]; + do { + Transaction transaction = new Transaction().withBinaryOutput().scan(cursor, options); + Object[] results = client.exec(transaction).get(); + cursor = (GlideString) ((Object[]) results[0])[0]; + keysFound = ArrayUtils.addAll(keysFound, (Object[]) ((Object[]) results[0])[1]); + } while (!cursor.equals(initialCursor)); + + assertTrue( + ArrayUtils.contains(keysFound, typeKeys.get(type)), + "Unable to find " + typeKeys.get(type) + " in a scan by type"); + + options = + ScanOptions.builder().matchPattern(typeKeys.get(type).toString()).count(42L).build(); + cursor = initialCursor; + keysFound = new Object[0]; + do { + Transaction transaction = new Transaction().withBinaryOutput().scan(cursor, options); + Object[] results = client.exec(transaction).get(); + cursor = (GlideString) ((Object[]) results[0])[0]; + keysFound = ArrayUtils.addAll(keysFound, (Object[]) ((Object[]) results[0])[1]); + } while (!cursor.equals(initialCursor)); + + assertTrue( + ArrayUtils.contains(keysFound, typeKeys.get(type)), + "Unable to find " + typeKeys.get(type) + " in a scan by match pattern"); + } + } + + @Test + @SneakyThrows + public void test_transaction_dump_restore() { + GlideString key1 = gs("{key}-1" + UUID.randomUUID()); + GlideString key2 = gs("{key}-2" + UUID.randomUUID()); + String value = UUID.randomUUID().toString(); + + // Setup + assertEquals(OK, client.set(key1, gs(value)).get()); + + // Verify dump + Transaction transaction = new Transaction().withBinaryOutput().dump(key1); + Object[] result = client.exec(transaction).get(); + GlideString payload = (GlideString) (result[0]); + + // Verify restore + transaction = new Transaction(); + transaction.restore(key2, 0, payload.getBytes()); + transaction.get(key2); + Object[] response = client.exec(transaction).get(); + assertEquals(OK, response[0]); + assertEquals(value, response[1]); + } + + @Test + @SneakyThrows + public void test_transaction_function_dump_restore() { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")); + String libName = "mylib"; + String funcName = "myfun"; + String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true); + + // Setup + client.functionLoad(code, true).get(); + + // Verify functionDump + Transaction transaction = new Transaction().withBinaryOutput().functionDump(); + Object[] result = client.exec(transaction).get(); + GlideString payload = (GlideString) (result[0]); + + // Verify functionRestore + transaction = new Transaction(); + transaction.functionRestore(payload.getBytes(), FunctionRestorePolicy.REPLACE); + Object[] response = client.exec(transaction).get(); + assertEquals(OK, response[0]); + } + + @Test + @SneakyThrows + public void test_transaction_xinfoStream() { + Transaction transaction = new Transaction(); + final String streamKey = "{streamKey}-" + UUID.randomUUID(); + LinkedHashMap expectedStreamInfo = + new LinkedHashMap<>() { + { + put("radix-tree-keys", 1L); + put("radix-tree-nodes", 2L); + put("length", 1L); + put("groups", 0L); + put("first-entry", new Object[] {"0-1", new Object[] {"field1", "value1"}}); + put("last-generated-id", "0-1"); + put("last-entry", new Object[] {"0-1", new Object[] {"field1", "value1"}}); + } + }; + LinkedHashMap expectedStreamFullInfo = + new LinkedHashMap<>() { + { + put("radix-tree-keys", 1L); + put("radix-tree-nodes", 2L); + put("entries", new Object[][] {{"0-1", new Object[] {"field1", "value1"}}}); + put("length", 1L); + put("groups", new Object[0]); + put("last-generated-id", "0-1"); + } + }; + + transaction + .xadd(streamKey, Map.of("field1", "value1"), StreamAddOptions.builder().id("0-1").build()) + .xinfoStream(streamKey) + .xinfoStreamFull(streamKey); + + Object[] results = client.exec(transaction).get(); + + if (SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedStreamInfo.put("max-deleted-entry-id", "0-0"); + expectedStreamInfo.put("entries-added", 1L); + expectedStreamInfo.put("recorded-first-entry-id", "0-1"); + expectedStreamFullInfo.put("max-deleted-entry-id", "0-0"); + expectedStreamFullInfo.put("entries-added", 1L); + expectedStreamFullInfo.put("recorded-first-entry-id", "0-1"); + } + + assertDeepEquals( + new Object[] { + "0-1", // xadd(streamKey1, Map.of("field1", "value1"), ... .id("0-1").build()); + expectedStreamInfo, // xinfoStream(streamKey) + expectedStreamFullInfo, // xinfoStreamFull(streamKey1) + }, + results); + } + + @SneakyThrows + @Test + public void binary_strings() { + String key = UUID.randomUUID().toString(); + client.set(key, "_").get(); + // use dump to ensure that we have non-string convertible bytes + var bytes = client.dump(gs(key)).get(); + + var transaction = new Transaction().withBinaryOutput().set(gs(key), gs(bytes)).get(gs(key)); + + var responses = client.exec(transaction).get(); + + assertDeepEquals( + new Object[] { + OK, gs(bytes), + }, + responses); + } } diff --git a/java/settings.gradle b/java/settings.gradle index d93b818e15..6d5e31d8a0 100644 --- a/java/settings.gradle +++ b/java/settings.gradle @@ -2,5 +2,4 @@ rootProject.name = 'glide' include 'client' include 'integTest' -include 'examples' include 'benchmarks' diff --git a/java/src/errors.rs b/java/src/errors.rs new file mode 100644 index 0000000000..93372a7257 --- /dev/null +++ b/java/src/errors.rs @@ -0,0 +1,110 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ +use jni::{errors::Error as JNIError, JNIEnv}; +use log::error; +use std::string::FromUtf8Error; + +pub enum FFIError { + Jni(JNIError), + Uds(String), + Utf8(FromUtf8Error), + Logger(String), +} + +impl From for FFIError { + fn from(value: jni::errors::Error) -> Self { + FFIError::Jni(value) + } +} + +impl From for FFIError { + fn from(value: FromUtf8Error) -> Self { + FFIError::Utf8(value) + } +} + +impl std::fmt::Display for FFIError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FFIError::Jni(err) => write!(f, "{}", err), + FFIError::Uds(err) => write!(f, "{}", err), + FFIError::Utf8(err) => write!(f, "{}", err), + FFIError::Logger(err) => write!(f, "{}", err), + } + } +} + +#[derive(Copy, Clone)] +pub enum ExceptionType { + Exception, + RuntimeException, +} + +impl std::fmt::Display for ExceptionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExceptionType::Exception => write!(f, "java/lang/Exception"), + ExceptionType::RuntimeException => write!(f, "java/lang/RuntimeException"), + } + } +} + +// This handles `FFIError`s by converting them to Java exceptions and throwing them. +pub fn handle_errors(env: &mut JNIEnv, result: Result) -> Option { + match result { + Ok(value) => Some(value), + Err(err) => { + match err { + FFIError::Utf8(utf8_error) => throw_java_exception( + env, + ExceptionType::RuntimeException, + &utf8_error.to_string(), + ), + error => throw_java_exception(env, ExceptionType::Exception, &error.to_string()), + }; + // Return `None` because we need to still return a value after throwing. + // This signals to the caller that we need to return the default value. + None + } + } +} + +// This function handles Rust panics by converting them into Java exceptions and throwing them. +// `func` returns an `Option` because this is intended to wrap the output of `handle_errors`. +pub fn handle_panics Option>( + func: F, + ffi_func_name: &str, +) -> Option { + match std::panic::catch_unwind(func) { + Ok(value) => value, + Err(_err) => { + // Following https://github.com/jni-rs/jni-rs/issues/76#issuecomment-363523906 + // and throwing a runtime exception is not feasible here because of https://github.com/jni-rs/jni-rs/issues/432 + error!("Native function {} panicked.", ffi_func_name); + None + } + } +} + +pub fn throw_java_exception(env: &mut JNIEnv, exception_type: ExceptionType, message: &str) { + match env.exception_check() { + Ok(true) => (), + Ok(false) => { + env.throw_new(exception_type.to_string(), message) + .unwrap_or_else(|err| { + error!( + "Failed to create exception with string {}: {}", + message, + err.to_string() + ); + }); + } + Err(err) => { + error!( + "Failed to check if an exception is currently being thrown: {}", + err.to_string() + ); + } + } +} diff --git a/java/src/ffi_test.rs b/java/src/ffi_test.rs index 199a811392..03b4e52726 100644 --- a/java/src/ffi_test.rs +++ b/java/src/ffi_test.rs @@ -1,9 +1,10 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +use crate::errors::{handle_errors, handle_panics, throw_java_exception, ExceptionType, FFIError}; use jni::{ - objects::{JClass, JLongArray}, - sys::jlong, + objects::{JByteArray, JClass, JLongArray, JString}, + sys::{jboolean, jdouble, jlong}, JNIEnv, }; use redis::Value; @@ -21,7 +22,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedNil<'local>( pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedSimpleString<'local>( mut env: JNIEnv<'local>, _class: JClass<'local>, - value: jni::objects::JString<'local>, + value: JString<'local>, ) -> jlong { let value: String = env.get_string(&value).unwrap().into(); let redis_value = Value::SimpleString(value); @@ -51,7 +52,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedInt<'local>( pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedBulkString<'local>( env: JNIEnv<'local>, _class: JClass<'local>, - value: jni::objects::JByteArray<'local>, + value: JByteArray<'local>, ) -> jlong { let value = env.convert_byte_array(&value).unwrap(); let value = value.into_iter().collect::>(); @@ -88,7 +89,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedMap<'local>( pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedDouble<'local>( _env: JNIEnv<'local>, _class: JClass<'local>, - value: jni::sys::jdouble, + value: jdouble, ) -> jlong { let redis_value = Value::Double(value.into()); Box::leak(Box::new(redis_value)) as *mut Value as jlong @@ -98,7 +99,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedDouble<'local>( pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedBoolean<'local>( _env: JNIEnv<'local>, _class: JClass<'local>, - value: jni::sys::jboolean, + value: jboolean, ) -> jlong { let redis_value = Value::Boolean(value != 0); Box::leak(Box::new(redis_value)) as *mut Value as jlong @@ -108,7 +109,7 @@ pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedBoolean<'local>( pub extern "system" fn Java_glide_ffi_FfiTest_createLeakedVerbatimString<'local>( mut env: JNIEnv<'local>, _class: JClass<'local>, - value: jni::objects::JString<'local>, + value: JString<'local>, ) -> jlong { use redis::VerbatimFormat; let value: String = env.get_string(&value).unwrap().into(); @@ -144,3 +145,68 @@ fn java_long_array_to_value<'local>( .map(|value| Value::Int(*value)) .collect::>() } + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_FfiTest_handlePanics<'local>( + _env: JNIEnv<'local>, + _class: JClass<'local>, + should_panic: jboolean, + error_present: jboolean, + value: jlong, + default_value: jlong, +) -> jlong { + let should_panic = should_panic != 0; + let error_present = error_present != 0; + handle_panics( + || { + if should_panic { + panic!("Panicking") + } else if error_present { + None + } else { + Some(value) + } + }, + "handlePanics", + ) + .unwrap_or(default_value) +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_FfiTest_handleErrors<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + is_success: jboolean, + value: jlong, + default_value: jlong, +) -> jlong { + let is_success = is_success != 0; + let error = FFIError::Uds("Error starting socket listener".to_string()); + let result = if is_success { Ok(value) } else { Err(error) }; + handle_errors(&mut env, result).unwrap_or(default_value) +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_FfiTest_throwException<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + throw_twice: jboolean, + is_runtime_exception: jboolean, + message: JString<'local>, +) { + let throw_twice = throw_twice != 0; + let is_runtime_exception = is_runtime_exception != 0; + + let exception_type = if is_runtime_exception { + ExceptionType::RuntimeException + } else { + ExceptionType::Exception + }; + + let message: String = env.get_string(&message).unwrap().into(); + throw_java_exception(&mut env, exception_type, &message); + + if throw_twice { + throw_java_exception(&mut env, exception_type, &message); + } +} diff --git a/java/src/lib.rs b/java/src/lib.rs index eb81b165f1..467a6f9be8 100644 --- a/java/src/lib.rs +++ b/java/src/lib.rs @@ -1,170 +1,323 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -use glide_core::start_socket_listener; +use glide_core::start_socket_listener as start_socket_listener_core; -use jni::objects::{JClass, JObject, JObjectArray, JString, JThrowable}; -use jni::sys::jlong; +// Protocol constants to expose to Java. +use glide_core::client::FINISHED_SCAN_CURSOR; +use glide_core::HASH as TYPE_HASH; +use glide_core::LIST as TYPE_LIST; +use glide_core::MAX_REQUEST_ARGS_LENGTH as MAX_REQUEST_ARGS_LENGTH_IN_BYTES; +use glide_core::SET as TYPE_SET; +use glide_core::STREAM as TYPE_STREAM; +use glide_core::STRING as TYPE_STRING; +use glide_core::ZSET as TYPE_ZSET; + +use bytes::Bytes; +use jni::errors::Error as JniError; +use jni::objects::{JByteArray, JClass, JObject, JObjectArray, JString}; +use jni::sys::{jint, jlong, jsize}; use jni::JNIEnv; -use log::error; use redis::Value; use std::sync::mpsc; +mod errors; + +use errors::{handle_errors, handle_panics, FFIError}; + #[cfg(ffi_test)] mod ffi_test; #[cfg(ffi_test)] pub use ffi_test::*; +struct Level(i32); + // TODO: Consider caching method IDs here in a static variable (might need RwLock to mutate) -fn redis_value_to_java<'local>(env: &mut JNIEnv<'local>, val: Value) -> JObject<'local> { +fn redis_value_to_java<'local>( + env: &mut JNIEnv<'local>, + val: Value, + encoding_utf8: bool, +) -> Result, FFIError> { match val { - Value::Nil => JObject::null(), - Value::SimpleString(str) => JObject::from(env.new_string(str).unwrap()), - Value::Okay => JObject::from(env.new_string("OK").unwrap()), - Value::Int(num) => env - .new_object("java/lang/Long", "(J)V", &[num.into()]) - .unwrap(), - Value::BulkString(data) => match std::str::from_utf8(data.as_ref()) { - Ok(val) => JObject::from(env.new_string(val).unwrap()), - Err(_err) => { - let _ = env.throw("Error decoding Unicode data"); - JObject::null() + Value::Nil => Ok(JObject::null()), + Value::SimpleString(data) => { + if encoding_utf8 { + Ok(JObject::from(env.new_string(data)?)) + } else { + Ok(JObject::from(env.byte_array_from_slice(data.as_bytes())?)) } - }, - Value::Array(array) => { - let items: JObjectArray = env - .new_object_array(array.len() as i32, "java/lang/Object", JObject::null()) - .unwrap(); - - for (i, item) in array.into_iter().enumerate() { - let java_value = redis_value_to_java(env, item); - env.set_object_array_element(&items, i as i32, java_value) - .unwrap(); + } + Value::Okay => Ok(JObject::from(env.new_string("OK")?)), + Value::Int(num) => Ok(env.new_object("java/lang/Long", "(J)V", &[num.into()])?), + Value::BulkString(data) => { + if encoding_utf8 { + let utf8_str = String::from_utf8(data)?; + Ok(JObject::from(env.new_string(utf8_str)?)) + } else { + Ok(JObject::from(env.byte_array_from_slice(&data)?)) } - - items.into() } + Value::Array(array) => array_to_java_array(env, array, encoding_utf8), Value::Map(map) => { - let linked_hash_map = env - .new_object("java/util/LinkedHashMap", "()V", &[]) - .unwrap(); + let linked_hash_map = env.new_object("java/util/LinkedHashMap", "()V", &[])?; for (key, value) in map { - let java_key = redis_value_to_java(env, key); - let java_value = redis_value_to_java(env, value); + let java_key = redis_value_to_java(env, key, encoding_utf8)?; + let java_value = redis_value_to_java(env, value, encoding_utf8)?; env.call_method( &linked_hash_map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", &[(&java_key).into(), (&java_value).into()], - ) - .unwrap(); + )?; } - linked_hash_map + Ok(linked_hash_map) + } + Value::Double(float) => Ok(env.new_object("java/lang/Double", "(D)V", &[float.into()])?), + Value::Boolean(bool) => Ok(env.new_object("java/lang/Boolean", "(Z)V", &[bool.into()])?), + Value::VerbatimString { format: _, text } => { + if encoding_utf8 { + Ok(JObject::from(env.new_string(text)?)) + } else { + Ok(JObject::from(env.byte_array_from_slice(text.as_bytes())?)) + } } - Value::Double(float) => env - .new_object("java/lang/Double", "(D)V", &[float.into()]) - .unwrap(), - Value::Boolean(bool) => env - .new_object("java/lang/Boolean", "(Z)V", &[bool.into()]) - .unwrap(), - Value::VerbatimString { format: _, text } => JObject::from(env.new_string(text).unwrap()), Value::BigNumber(_num) => todo!(), Value::Set(array) => { - let set = env.new_object("java/util/HashSet", "()V", &[]).unwrap(); + let set = env.new_object("java/util/HashSet", "()V", &[])?; for elem in array { - let java_value = redis_value_to_java(env, elem); + let java_value = redis_value_to_java(env, elem, encoding_utf8)?; env.call_method( &set, "add", "(Ljava/lang/Object;)Z", &[(&java_value).into()], - ) - .unwrap(); + )?; } - set + Ok(set) } Value::Attribute { data: _, attributes: _, } => todo!(), - Value::Push { kind: _, data: _ } => todo!(), + // Create a java `Map` with two keys: + // - "kind" which corresponds to the push type, stored as a `String` + // - "values" which corresponds to the array of values received, stored as `Object[]` + // Only string messages are supported now by Redis and `redis-rs`. + Value::Push { kind, data } => { + let hash_map = env.new_object("java/util/HashMap", "()V", &[])?; + + let kind_str = env.new_string("kind")?; + let kind_value_str = env.new_string(format!("{kind:?}"))?; + + let _ = env.call_method( + &hash_map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[(&kind_str).into(), (&kind_value_str).into()], + )?; + + let values_str = env.new_string("values")?; + let values = array_to_java_array(env, data, encoding_utf8)?; + + let _ = env.call_method( + &hash_map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[(&values_str).into(), (&values).into()], + )?; + + Ok(hash_map) + } } } +/// Convert an array of values into java array of corresponding values. +/// +/// Recursively calls to [`redis_value_to_java`] for every element. +/// +/// Returns an arbitrary java `Object[]`. +fn array_to_java_array<'local>( + env: &mut JNIEnv<'local>, + values: Vec, + encoding_utf8: bool, +) -> Result, FFIError> { + let items: JObjectArray = + env.new_object_array(values.len() as i32, "java/lang/Object", JObject::null())?; + + for (i, item) in values.into_iter().enumerate() { + let java_value = redis_value_to_java(env, item, encoding_utf8)?; + env.set_object_array_element(&items, i as i32, java_value)?; + } + + Ok(items.into()) +} + #[no_mangle] -pub extern "system" fn Java_glide_ffi_resolvers_RedisValueResolver_valueFromPointer<'local>( +pub extern "system" fn Java_glide_ffi_resolvers_GlideValueResolver_valueFromPointer<'local>( mut env: JNIEnv<'local>, _class: JClass<'local>, pointer: jlong, ) -> JObject<'local> { - let value = unsafe { Box::from_raw(pointer as *mut Value) }; - redis_value_to_java(&mut env, *value) + handle_panics( + move || { + fn value_from_pointer<'a>( + env: &mut JNIEnv<'a>, + pointer: jlong, + ) -> Result, FFIError> { + let value = unsafe { Box::from_raw(pointer as *mut Value) }; + redis_value_to_java(env, *value, true) + } + let result = value_from_pointer(&mut env, pointer); + handle_errors(&mut env, result) + }, + "valueFromPointer", + ) + .unwrap_or(JObject::null()) } #[no_mangle] -pub extern "system" fn Java_glide_ffi_resolvers_SocketListenerResolver_startSocketListener< +pub extern "system" fn Java_glide_ffi_resolvers_GlideValueResolver_valueFromPointerBinary< 'local, >( - env: JNIEnv<'local>, + mut env: JNIEnv<'local>, _class: JClass<'local>, + pointer: jlong, ) -> JObject<'local> { - let (tx, rx) = mpsc::channel::>(); + handle_panics( + move || { + fn value_from_pointer_binary<'a>( + env: &mut JNIEnv<'a>, + pointer: jlong, + ) -> Result, FFIError> { + let value = unsafe { Box::from_raw(pointer as *mut Value) }; + redis_value_to_java(env, *value, false) + } + let result = value_from_pointer_binary(&mut env, pointer); + handle_errors(&mut env, result) + }, + "valueFromPointerBinary", + ) + .unwrap_or(JObject::null()) +} - start_socket_listener(move |socket_path: Result| { - // Signals that thread has started - let _ = tx.send(socket_path); - }); +/// Creates a leaked vector of byte arrays representing the args and returns a handle to it. +/// +/// This function is meant to be invoked by Java using JNI. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +/// * `args` - The arguments. This should be a byte[][] from Java. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_GlideValueResolver_createLeakedBytesVec<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + args: JObjectArray<'local>, +) -> jlong { + handle_panics( + move || { + fn create_leaked_bytes_vec<'a>( + env: &mut JNIEnv<'a>, + args: JObjectArray<'a>, + ) -> Result { + let num_elements = env.get_array_length(&args)?; + let mut bytes_vec = Vec::with_capacity(num_elements as usize); - // Wait until the thread has started - let socket_path = rx.recv(); + for index in 0..num_elements { + let value = env.get_object_array_element(&args, index as jsize)?; + bytes_vec.push(Bytes::from( + env.convert_byte_array(JByteArray::from(value))?, + )) + } + Ok(Box::leak(Box::new(bytes_vec)) as *mut Vec as jlong) + } + let result = create_leaked_bytes_vec(&mut env, args); + handle_errors(&mut env, result) + }, + "createLeakedBytesVec", + ) + .unwrap_or(0) +} - match socket_path { - Ok(Ok(path)) => env.new_string(path).unwrap().into(), - Ok(Err(error_message)) => { - throw_java_exception(env, error_message); - JObject::null() - } - Err(error) => { - throw_java_exception(env, error.to_string()); - JObject::null() - } - } +/// Returns the maximum total length in bytes of request arguments. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `_env` - The JNI environment. Not used. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_GlideValueResolver_getMaxRequestArgsLengthInBytes< + 'local, +>( + _env: JNIEnv<'local>, + _class: JClass<'local>, +) -> jlong { + MAX_REQUEST_ARGS_LENGTH_IN_BYTES as jlong } -fn throw_java_exception(mut env: JNIEnv, message: String) { - let res = env.new_object( - "java/lang/Exception", - "(Ljava/lang/String;)V", - &[(&env.new_string(message.clone()).unwrap()).into()], - ); +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_SocketListenerResolver_startSocketListener< + 'local, +>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JObject<'local> { + handle_panics( + move || { + fn start_socket_listener<'a>(env: &mut JNIEnv<'a>) -> Result, FFIError> { + let (tx, rx) = mpsc::channel::>(); - match res { - Ok(res) => { - let _ = env.throw(JThrowable::from(res)); - } - Err(err) => { - error!( - "Failed to create exception with string {}: {}", - message, - err.to_string() - ); - } - }; + start_socket_listener_core(move |socket_path: Result| { + // Signals that thread has started + let _ = tx.send(socket_path); + }); + + // Wait until the thread has started + let socket_path = rx.recv(); + + match socket_path { + Ok(Ok(path)) => env + .new_string(path) + .map(|p| p.into()) + .map_err(|err| FFIError::Uds(err.to_string())), + Ok(Err(error_message)) => Err(FFIError::Uds(error_message)), + Err(error) => Err(FFIError::Uds(error.to_string())), + } + } + let result = start_socket_listener(&mut env); + handle_errors(&mut env, result) + }, + "startSocketListener", + ) + .unwrap_or(JObject::null()) } #[no_mangle] pub extern "system" fn Java_glide_ffi_resolvers_ScriptResolver_storeScript<'local>( mut env: JNIEnv<'local>, _class: JClass<'local>, - code: JString, + code: JByteArray, ) -> JObject<'local> { - let code_str: String = env.get_string(&code).unwrap().into(); - let hash = glide_core::scripts_container::add_script(&code_str); - JObject::from(env.new_string(hash).unwrap()) + handle_panics( + move || { + fn store_script<'a>( + env: &mut JNIEnv<'a>, + code: JByteArray, + ) -> Result, FFIError> { + let code_byte_array = env.convert_byte_array(code)?; + let hash = glide_core::scripts_container::add_script(&code_byte_array); + Ok(JObject::from(env.new_string(hash)?)) + } + let result = store_script(&mut env, code); + handle_errors(&mut env, result) + }, + "storeScript", + ) + .unwrap_or(JObject::null()) } #[no_mangle] @@ -173,6 +326,283 @@ pub extern "system" fn Java_glide_ffi_resolvers_ScriptResolver_dropScript<'local _class: JClass<'local>, hash: JString, ) { - let hash_str: String = env.get_string(&hash).unwrap().into(); - glide_core::scripts_container::remove_script(&hash_str); + handle_panics( + move || { + fn drop_script(env: &mut JNIEnv<'_>, hash: JString) -> Result<(), FFIError> { + let hash_str: String = env.get_string(&hash)?.into(); + glide_core::scripts_container::remove_script(&hash_str); + Ok(()) + } + let result = drop_script(&mut env, hash); + handle_errors(&mut env, result) + }, + "dropScript", + ) + .unwrap_or(()) +} + +// TODO: Add DISABLED level here once it is added to logger-core +impl From for Level { + fn from(level: logger_core::Level) -> Self { + match level { + logger_core::Level::Error => Level(0), + logger_core::Level::Warn => Level(1), + logger_core::Level::Info => Level(2), + logger_core::Level::Debug => Level(3), + logger_core::Level::Trace => Level(4), + } + } +} + +impl TryFrom for logger_core::Level { + type Error = FFIError; + fn try_from(level: Level) -> Result>::Error> { + // TODO: Add DISABLED level here once it is added to logger-core + match level.0 { + 0 => Ok(logger_core::Level::Error), + 1 => Ok(logger_core::Level::Warn), + 2 => Ok(logger_core::Level::Info), + 3 => Ok(logger_core::Level::Debug), + 4 => Ok(logger_core::Level::Trace), + _ => Err(FFIError::Logger(format!( + "Invalid log level: {:?}", + level.0 + ))), + } + } +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_LoggerResolver_logInternal<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + level: jint, + log_identifier: JString<'local>, + message: JString<'local>, +) { + handle_panics( + move || { + fn log_internal( + env: &mut JNIEnv<'_>, + level: jint, + log_identifier: JString<'_>, + message: JString<'_>, + ) -> Result<(), FFIError> { + let level = Level(level); + + let log_identifier: String = env.get_string(&log_identifier)?.into(); + + let message: String = env.get_string(&message)?.into(); + + logger_core::log(level.try_into()?, log_identifier, message); + Ok(()) + } + let result = log_internal(&mut env, level, log_identifier, message); + handle_errors(&mut env, result) + }, + "logInternal", + ) + .unwrap_or(()) +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_LoggerResolver_initInternal<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + level: jint, + file_name: JString<'local>, +) -> jint { + handle_panics( + move || { + fn init_internal( + env: &mut JNIEnv<'_>, + level: jint, + file_name: JString<'_>, + ) -> Result { + let level = if level >= 0 { Some(level) } else { None }; + let file_name: Option = match env.get_string(&file_name) { + Ok(file_name) => Some(file_name.into()), + Err(JniError::NullPtr(_)) => None, + Err(err) => return Err(err.into()), + }; + let level = match level { + Some(lvl) => Some(Level(lvl).try_into()?), + None => None, + }; + let logger_level = logger_core::init(level, file_name.as_deref()); + Ok(Level::from(logger_level).0) + } + let result = init_internal(&mut env, level, file_name); + handle_errors(&mut env, result) + }, + "initInternal", + ) + .unwrap_or(0) +} + +/// Releases a ClusterScanCursor handle allocated in Rust. +/// +/// This function is meant to be invoked by Java using JNI. +/// +/// * `_env` - The JNI environment. Not used. +/// * `_class` - The class object. Not used. +/// * cursor - The cursor handle to release. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ClusterScanCursorResolver_releaseNativeCursor< + 'local, +>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + cursor: JString<'local>, +) { + handle_panics( + move || { + fn release_native_cursor( + env: &mut JNIEnv<'_>, + cursor: JString<'_>, + ) -> Result<(), FFIError> { + let cursor_str: String = env.get_string(&cursor)?.into(); + glide_core::cluster_scan_container::remove_scan_state_cursor(cursor_str); + Ok(()) + } + let result = release_native_cursor(&mut env, cursor); + handle_errors(&mut env, result) + }, + "releaseNativeCursor", + ) + .unwrap_or(()) +} + +/// Returns the String representing a finished cursor handle. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ClusterScanCursorResolver_getFinishedCursorHandleConstant< + 'local, +>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, FINISHED_SCAN_CURSOR, "getFinishedCursorHandleConstant") +} + +/// Returns the String representing the name of the ObjectType String. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ObjectTypeResolver_getTypeStringConstant<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, TYPE_STRING, "getTypeStringConstant") +} + +/// Returns the String representing the name of the ObjectType List. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ObjectTypeResolver_getTypeListConstant<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, TYPE_LIST, "getTypeListConstant") +} + +/// Returns the String representing the name of the ObjectType Set. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ObjectTypeResolver_getTypeSetConstant<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, TYPE_SET, "getTypeSetConstant") +} + +/// Returns the String representing the name of the ObjectType ZSet. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ObjectTypeResolver_getTypeZSetConstant<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, TYPE_ZSET, "getTypeZSetConstant") +} + +/// Returns the String representing the name of the ObjectType Hash. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ObjectTypeResolver_getTypeHashConstant<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, TYPE_HASH, "getTypeHashConstant") +} + +/// Returns the String representing the name of the ObjectType Set. +/// +/// This function is meant to be invoked by Java using JNI. This is used to ensure +/// that this constant is consistent with the Rust client. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_ObjectTypeResolver_getTypeStreamConstant<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> JString<'local> { + safe_create_jstring(env, TYPE_STREAM, "getTypeStreamConstant") +} + +/// Convert a Rust string to a Java String and handle errors. +/// +/// * `env` - The JNI environment. +/// * `_class` - The class object. Not used. +/// * `input` - The String to convert. +/// * `functionName` - The name of the calling function. +fn safe_create_jstring<'local>( + mut env: JNIEnv<'local>, + input: &str, + function_name: &str, +) -> JString<'local> { + handle_panics( + move || { + fn create_jstring<'a>( + env: &mut JNIEnv<'a>, + input: &str, + ) -> Result, FFIError> { + Ok(env.new_string(input)?) + } + let result = create_jstring(&mut env, input); + handle_errors(&mut env, result) + }, + function_name, + ) + .unwrap_or(JString::<'_>::default()) } diff --git a/logger_core/Cargo.toml b/logger_core/Cargo.toml index 74e0291561..be306d4751 100644 --- a/logger_core/Cargo.toml +++ b/logger_core/Cargo.toml @@ -3,7 +3,7 @@ name = "logger_core" version = "0.1.0" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] [lib] diff --git a/logger_core/src/lib.rs b/logger_core/src/lib.rs index 8c0d85de0a..7835ab7a6f 100644 --- a/logger_core/src/lib.rs +++ b/logger_core/src/lib.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use once_cell::sync::OnceCell; use std::sync::RwLock; diff --git a/logger_core/tests/test_logger.rs b/logger_core/tests/test_logger.rs index 7481a19046..e38d91e92b 100644 --- a/logger_core/tests/test_logger.rs +++ b/logger_core/tests/test_logger.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use test_env_helpers::*; diff --git a/node/.gitignore b/node/.gitignore index 1b023a1ae7..1cae12ee73 100644 --- a/node/.gitignore +++ b/node/.gitignore @@ -138,3 +138,5 @@ yarn.lock src/ProtobufMessage* .npmignore + +package.json.tmpl diff --git a/node/.prettierignore b/node/.prettierignore new file mode 100644 index 0000000000..6ced842400 --- /dev/null +++ b/node/.prettierignore @@ -0,0 +1,6 @@ +# ignore that dir, because there are a lot of files which we don't manage, e.g. json files in cargo crates +rust-client/* +*.md +# unignore specific files +!rust-client/package.json +!rust-client/tsconfig.json diff --git a/node/DEVELOPER.md b/node/DEVELOPER.md index dfbe4b4172..dba47cdef7 100644 --- a/node/DEVELOPER.md +++ b/node/DEVELOPER.md @@ -1,6 +1,6 @@ # Developer Guide -This document describes how to set up your development environment to build and test the GLIDE for Redis Node wrapper. +This document describes how to set up your development environment to build and test Valkey GLIDE Node wrapper. ### Development Overview @@ -12,7 +12,9 @@ The GLIDE Node wrapper consists of both TypeScript and Rust code. Rust bindings Software Dependencies -> If your NodeJS version is below the supported version specified in the client's [documentation](https://github.com/aws/glide-for-redis/blob/main/node/README.md#nodejs-supported-version), you can upgrade it using [NVM](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script). +##### **Note:** Nodejs Supported Version + +If your Nodejs version is below the supported version specified in the client's [documentation](https://github.com/valkey-io/valkey-glide/blob/main/node/README.md#nodejs-supported-version), you can upgrade it using [NVM](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script). - npm - git @@ -30,8 +32,12 @@ sudo apt update -y sudo apt install -y nodejs npm git gcc pkg-config protobuf-compiler openssl libssl-dev curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" +# Check the installed node version +node -v ``` +> **Note:** Ensure that you installed a supported Node.js version. For Ubuntu 22.04 or earlier, please refer to the instructions [here](#note-nodejs-supported-version) to upgrade your Node.js version. + **Dependencies installation for CentOS** ```bash @@ -57,8 +63,8 @@ Before starting this step, make sure you've installed all software requirments. 1. Clone the repository: ```bash VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git - cd glide-for-redis + git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git + cd valkey-glide ``` 2. Initialize git submodule: ```bash @@ -95,9 +101,10 @@ Before starting this step, make sure you've installed all software requirments. Once building completed, you'll find the compiled JavaScript code in the`./build-ts` folder. 5. Run tests: - 1. Ensure that you have installed redis-server and redis-cli on your host. You can find the Redis installation guide at the following link: [Redis Installation Guide](https://redis.io/docs/install/install-redis/install-redis-on-linux/). + 1. Ensure that you have installed server and valkey-cli on your host. You can download Valkey at the following link: [Valkey Download page](https://valkey.io/download/). 2. Execute the following command from the node folder: ```bash + npm run build # make sure we have a debug build compiled first npm test ``` 6. Integrating the built GLIDE package into your project: @@ -112,7 +119,7 @@ Before starting this step, make sure you've installed all software requirments. ### Troubleshooting -- If the build fails after running `npx tsc` because `glide-rs` isn't found, check if your npm version is in the range 9.0.0-9.4.1, and if so, upgrade it. 9.4.2 contains a fix to a change introduced in 9.0.0 that is required in order to build the library. +- If the build fails after running `npx tsc` because `redis-rs` isn't found, check if your npm version is in the range 9.0.0-9.4.1, and if so, upgrade it. 9.4.2 contains a fix to a change introduced in 9.0.0 that is required in order to build the library. ### Test @@ -122,6 +129,12 @@ To run tests, use the following command: npm test ``` +To run the integration tests with existing servers, run the following command: + +```bash +npm run test -- --cluster-endpoints=localhost:7000 --standalone-endpoints=localhost:6379 +``` + ### Submodules After pulling new changes, ensure that you update the submodules by running the following command: @@ -149,13 +162,34 @@ Development on the Node wrapper may involve changes in either the TypeScript or #### Running the linters 1. TypeScript + ```bash - # Run from the `node` folder + # Run from the root folder of the GLIDE repository npm install eslint-plugin-import@latest @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-tsdoc eslint typescript eslint-plugin-import@latest eslint-config-prettier prettier npm i + cd node npx eslint . --max-warnings=0 npx prettier --check . ``` + + To automatically apply prettier recommendations, run the following command: + + ```bash + npx prettier -w . + ``` + + To avoid getting ESLint warnings from protobuf generated files, run the following command: + + ```bash + npx eslint --ignore-pattern ProtobufMessage.* . + ``` + + To automatically apply ESLint recommendations, run the following command: + + ```bash + npx eslint --ignore-pattern ProtobufMessage.* --ignore-pattern 'build-ts/**' --fix . + ``` + 2. Rust ```bash # Run from the `node/rust-client` folder diff --git a/node/README.md b/node/README.md index a6bba1e409..6d98b6a856 100644 --- a/node/README.md +++ b/node/README.md @@ -1,93 +1,39 @@ -# GLIDE for Redis +# Valkey GLIDE -General Language Independent Driver for the Enterprise (GLIDE) for Redis, is an AWS-sponsored, open-source Redis client. GLIDE for Redis works with any Redis distribution that adheres to the Redis Serialization Protocol (RESP) specification, including open-source Redis, Amazon ElastiCache for Redis, and Amazon MemoryDB for Redis. -Strategic, mission-critical Redis-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Redis is designed to provide a client experience that helps meet these objectives. It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Redis-compatible services used by hundreds of thousands of customers. To help ensure consistency in development and operations, GLIDE for Redis is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. In this Preview release, GLIDE for Redis is available for Python and Javascript (Node.js), with support for Java actively under development. +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. -## Supported Redis Versions +## Supported Engine Versions -GLIDE for Redis is API-compatible with open source Redis version 6 and 7. +Refer to the [Supported Engine Versions table](https://github.com/valkey-io/valkey-glide/blob/main/README.md#supported-engine-versions) for details. ## Current Status -We've made GLIDE for Redis an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. +We've made Valkey GLIDE an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. This preview release is recommended for testing purposes only. # Getting Started - Node Wrapper ## System Requirements -The beta release of GLIDE for Redis was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. +In this release, Valkey GLIDE is available for Python and Java. Support for Node.js is actively under development, with plans to include more programming languages in the future. We're tracking future features on the [roadmap](https://github.com/orgs/aws/projects/187/). ## NodeJS supported version Node.js 16.20 or higher. -## Installation and Setup - -### Installing via Package Manager (npm) - -To install GLIDE for Redis using `npm`, follow these steps: - -1. Open your terminal. -2. Execute the command below: - ```bash - $ npm install @aws/glide-for-redis - ``` -3. After installation, confirm the client is installed by running: - ```bash - $ npm list - myApp@ /home/ubuntu/myApp - └── @aws/glide-for-redis@0.1.0 - ``` - -## Basic Examples - -#### Cluster Redis: - -```node -import { RedisClusterClient } from "@aws/glide-for-redis"; - -const addresses = [ - { - host: "redis.example.com", - port: 6379, - }, -]; -const client = await RedisClusterClient.createClient({ - addresses: addresses, -}); -await client.set("foo", "bar"); -const value = await client.get("foo"); -client.close(); -``` - -#### Standalone Redis: - -```node -import { RedisClient } from "@aws/glide-for-redis"; - -const addresses = [ - { - host: "redis_primary.example.com", - port: 6379, - }, - { - host: "redis_replica.example.com", - port: 6379, - }, -]; -const client = await RedisClient.createClient({ - addresses: addresses, -}); -await client.set("foo", "bar"); -const value = await client.get("foo"); -client.close(); -``` - ## Documentation -Visit our [wiki](https://github.com/aws/glide-for-redis/wiki/NodeJS-wrapper) for examples and further details on TLS, Read strategy, Timeouts and various other configurations. +Visit our [wiki](https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper) for examples and further details on TLS, Read strategy, Timeouts and various other configurations. ### Building & Testing -Development instructions for local building & testing the package are in the [DEVELOPER.md](https://github.com/aws/glide-for-redis/blob/main/node/DEVELOPER.md#build-from-source) file. +Development instructions for local building & testing the package are in the [DEVELOPER.md](https://github.com/valkey-io/valkey-glide/blob/main/node/DEVELOPER.md#build-from-source) file. + +### Supported platforms + +Currentlly the package is supported on: + +| Operation systems | C lib | Architecture | +| ----------------- | -------------------- | ----------------- | +| `Linux` | `glibc`, `musl libc` | `x86_64`, `arm64` | +| `macOS` | `Darwin` | `x86_64`, `arm64` | diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index 92d567c324..8f750e1968 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -212,7 +212,7 @@ The applicable license information is listed below: ---- -Package: addr2line:0.21.0 +Package: addr2line:0.22.0 The following copyrights and licenses were found in the source code of this package: @@ -1880,7 +1880,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: arcstr:1.1.5 +Package: arcstr:1.2.0 The following copyrights and licenses were found in the source code of this package: @@ -2129,7 +2129,7 @@ the following restrictions: ---- -Package: async-trait:0.1.80 +Package: async-trait:0.1.81 The following copyrights and licenses were found in the source code of this package: @@ -2587,7 +2587,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: backtrace:0.3.71 +Package: backtrace:0.3.73 The following copyrights and licenses were found in the source code of this package: @@ -2816,7 +2816,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: base64:0.22.0 +Package: base64:0.22.1 The following copyrights and licenses were found in the source code of this package: @@ -3045,7 +3045,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bitflags:1.3.2 +Package: bitflags:2.6.0 The following copyrights and licenses were found in the source code of this package: @@ -3274,7 +3274,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bitflags:2.5.0 +Package: bumpalo:3.16.0 The following copyrights and licenses were found in the source code of this package: @@ -3503,7 +3503,84 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bumpalo:3.16.0 +Package: byteorder:1.5.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + -- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +---- + +Package: bytes:1.6.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: cfg-if:1.0.0 The following copyrights and licenses were found in the source code of this package: @@ -3732,84 +3809,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: byteorder:1.5.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -- - -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to - ----- - -Package: bytes:1.6.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: cfg-if:1.0.0 +Package: chrono:0.4.38 The following copyrights and licenses were found in the source code of this package: @@ -4038,7 +4038,57 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: chrono:0.4.38 +Package: combine:4.6.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: convert_case:0.6.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: core-foundation:0.9.4 The following copyrights and licenses were found in the source code of this package: @@ -4267,57 +4317,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: combine:4.6.7 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: convert_case:0.6.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: core-foundation:0.9.4 +Package: core-foundation-sys:0.8.6 The following copyrights and licenses were found in the source code of this package: @@ -4546,7 +4546,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: crc16:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: crc32fast:1.4.2 The following copyrights and licenses were found in the source code of this package: @@ -4775,32 +4800,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crc16:0.4.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: crc32fast:1.4.0 +Package: crossbeam-channel:0.5.13 The following copyrights and licenses were found in the source code of this package: @@ -5029,7 +5029,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-channel:0.5.12 +Package: crossbeam-utils:0.8.20 The following copyrights and licenses were found in the source code of this package: @@ -5258,7 +5258,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-utils:0.8.19 +Package: ctor:0.2.8 The following copyrights and licenses were found in the source code of this package: @@ -5487,7 +5487,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ctor:0.2.8 +Package: deranged:0.3.11 The following copyrights and licenses were found in the source code of this package: @@ -5716,7 +5716,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: deranged:0.3.11 +Package: derivative:2.2.0 The following copyrights and licenses were found in the source code of this package: @@ -5945,7 +5945,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: derivative:2.2.0 +Package: directories:4.0.1 The following copyrights and licenses were found in the source code of this package: @@ -6174,7 +6174,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: directories:4.0.1 +Package: dirs-sys:0.3.7 The following copyrights and licenses were found in the source code of this package: @@ -6403,7 +6403,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dirs-sys:0.3.7 +Package: dispose:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -6632,7 +6632,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dispose:0.5.0 +Package: dispose-derive:0.4.0 The following copyrights and licenses were found in the source code of this package: @@ -6861,7 +6861,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dispose-derive:0.4.0 +Package: fast-math:0.1.1 The following copyrights and licenses were found in the source code of this package: @@ -7090,7 +7090,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: fast-math:0.1.1 +Package: file-rotate:0.7.6 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: flate2:1.0.30 The following copyrights and licenses were found in the source code of this package: @@ -7319,32 +7344,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: file-rotate:0.7.5 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: flate2:1.0.28 +Package: form_urlencoded:1.2.1 The following copyrights and licenses were found in the source code of this package: @@ -7573,7 +7573,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: form_urlencoded:1.2.1 +Package: futures:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7802,7 +7802,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures:0.3.30 +Package: futures-channel:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8031,7 +8031,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-channel:0.3.30 +Package: futures-core:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8260,7 +8260,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-core:0.3.30 +Package: futures-executor:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8489,7 +8489,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-executor:0.3.30 +Package: futures-intrusive:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -8718,7 +8718,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-intrusive:0.5.0 +Package: futures-io:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8947,7 +8947,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-io:0.3.30 +Package: futures-macro:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9176,7 +9176,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-macro:0.3.30 +Package: futures-sink:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9405,7 +9405,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-sink:0.3.30 +Package: futures-task:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9634,7 +9634,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-task:0.3.30 +Package: futures-util:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9863,7 +9863,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-util:0.3.30 +Package: getrandom:0.2.15 The following copyrights and licenses were found in the source code of this package: @@ -10092,7 +10092,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: getrandom:0.2.14 +Package: gimli:0.29.0 The following copyrights and licenses were found in the source code of this package: @@ -10321,7 +10321,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: gimli:0.28.1 +Package: glide-core:0.1.0 The following copyrights and licenses were found in the source code of this package: @@ -10527,30 +10527,9 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---- -Package: glide-core:0.1.0 +Package: hashbrown:0.14.5 The following copyrights and licenses were found in the source code of this package: @@ -10756,9 +10735,30 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---- -Package: hashbrown:0.14.3 +Package: heck:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -12132,7 +12132,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: instant:0.1.12 +Package: instant:0.1.13 The following copyrights and licenses were found in the source code of this package: @@ -12646,7 +12646,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: lazy_static:1.4.0 +Package: lazy_static:1.5.0 The following copyrights and licenses were found in the source code of this package: @@ -12875,7 +12875,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.153 +Package: libc:0.2.155 The following copyrights and licenses were found in the source code of this package: @@ -13104,7 +13104,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libloading:0.8.3 +Package: libloading:0.8.4 The following copyrights and licenses were found in the source code of this package: @@ -13147,7 +13147,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: lock_api:0.4.11 +Package: lock_api:0.4.12 The following copyrights and licenses were found in the source code of this package: @@ -13376,7 +13376,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: log:0.4.21 +Package: log:0.4.22 The following copyrights and licenses were found in the source code of this package: @@ -13813,7 +13813,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: memchr:2.7.2 +Package: memchr:2.7.4 The following copyrights and licenses were found in the source code of this package: @@ -13865,7 +13865,7 @@ For more information, please refer to ---- -Package: miniz_oxide:0.7.2 +Package: miniz_oxide:0.7.4 The following copyrights and licenses were found in the source code of this package: @@ -14139,7 +14139,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi:2.16.2 +Package: nanoid:0.4.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: napi:2.16.8 The following copyrights and licenses were found in the source code of this package: @@ -14189,7 +14214,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive:2.16.2 +Package: napi-derive:2.16.9 The following copyrights and licenses were found in the source code of this package: @@ -14214,7 +14239,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive-backend:1.0.64 +Package: napi-derive-backend:1.0.71 The following copyrights and licenses were found in the source code of this package: @@ -14289,7 +14314,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-bigint:0.4.4 +Package: num-bigint:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -14976,7 +15001,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-traits:0.2.18 +Package: num-traits:0.2.19 The following copyrights and licenses were found in the source code of this package: @@ -15434,7 +15459,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.32.2 +Package: object:0.36.1 The following copyrights and licenses were found in the source code of this package: @@ -16146,7 +16171,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: parking_lot:0.12.1 +Package: parking_lot:0.12.3 The following copyrights and licenses were found in the source code of this package: @@ -16375,7 +16400,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: parking_lot_core:0.9.9 +Package: parking_lot_core:0.9.10 The following copyrights and licenses were found in the source code of this package: @@ -18665,7 +18690,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: proc-macro2:1.0.81 +Package: proc-macro2:1.0.86 The following copyrights and licenses were found in the source code of this package: @@ -18894,7 +18919,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.4.0 +Package: protobuf:3.5.0 The following copyrights and licenses were found in the source code of this package: @@ -18919,7 +18944,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.4.0 +Package: protobuf-support:3.5.0 The following copyrights and licenses were found in the source code of this package: @@ -19891,7 +19916,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: redox_syscall:0.4.1 +Package: redox_syscall:0.5.3 The following copyrights and licenses were found in the source code of this package: @@ -19941,7 +19966,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: regex:1.10.4 +Package: regex:1.10.5 The following copyrights and licenses were found in the source code of this package: @@ -20170,7 +20195,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: regex-automata:0.4.6 +Package: regex-automata:0.4.7 The following copyrights and licenses were found in the source code of this package: @@ -20399,7 +20424,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: regex-syntax:0.8.3 +Package: regex-syntax:0.8.4 The following copyrights and licenses were found in the source code of this package: @@ -20628,7 +20653,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustc-demangle:0.1.23 +Package: rustc-demangle:0.1.24 The following copyrights and licenses were found in the source code of this package: @@ -20857,7 +20882,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls:0.22.3 +Package: rustls:0.22.4 The following copyrights and licenses were found in the source code of this package: @@ -21100,7 +21125,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.0 +Package: rustls-native-certs:0.7.1 The following copyrights and licenses were found in the source code of this package: @@ -21586,7 +21611,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.4.1 +Package: rustls-pki-types:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -21815,7 +21840,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.2 +Package: rustls-webpki:0.102.5 The following copyrights and licenses were found in the source code of this package: @@ -21833,7 +21858,7 @@ THIS SOFTWARE. ---- -Package: ryu:1.0.17 +Package: rustversion:1.0.17 The following copyrights and licenses were found in the source code of this package: @@ -22041,36 +22066,6 @@ The following copyrights and licenses were found in the source code of this pack -- -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - ----- - -Package: schannel:0.1.23 - -The following copyrights and licenses were found in the source code of this package: - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -22092,7 +22087,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: scopeguard:1.2.0 +Package: ryu:1.0.18 The following copyrights and licenses were found in the source code of this package: @@ -22300,6 +22295,36 @@ The following copyrights and licenses were found in the source code of this pack -- +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +---- + +Package: schannel:0.1.23 + +The following copyrights and licenses were found in the source code of this package: + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -22321,7 +22346,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: security-framework:2.10.0 +Package: scopeguard:1.2.0 The following copyrights and licenses were found in the source code of this package: @@ -22550,7 +22575,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: security-framework-sys:2.10.0 +Package: security-framework:2.11.1 The following copyrights and licenses were found in the source code of this package: @@ -22779,7 +22804,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: semver:1.0.22 +Package: security-framework-sys:2.11.1 The following copyrights and licenses were found in the source code of this package: @@ -23008,7 +23033,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.198 +Package: semver:1.0.23 The following copyrights and licenses were found in the source code of this package: @@ -23237,7 +23262,236 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.198 +Package: serde:1.0.204 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: serde_derive:1.0.204 The following copyrights and licenses were found in the source code of this package: @@ -23776,7 +24030,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: socket2:0.5.6 +Package: socket2:0.5.7 The following copyrights and licenses were found in the source code of this package: @@ -24030,7 +24284,57 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: subtle:2.5.0 +Package: strum:0.26.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: strum_macros:0.26.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: subtle:2.6.1 The following copyrights and licenses were found in the source code of this package: @@ -24290,7 +24594,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.60 +Package: syn:2.0.71 The following copyrights and licenses were found in the source code of this package: @@ -24519,7 +24823,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror:1.0.58 +Package: thiserror:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -24748,7 +25052,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.58 +Package: thiserror-impl:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -26351,7 +26655,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tinyvec:1.6.0 +Package: tinyvec:1.8.0 The following copyrights and licenses were found in the source code of this package: @@ -26849,7 +27153,7 @@ the following restrictions: ---- -Package: tokio:1.37.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: @@ -26874,7 +27178,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.2.0 +Package: tokio-macros:2.3.0 The following copyrights and licenses were found in the source code of this package: @@ -27153,7 +27457,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-util:0.7.10 +Package: tokio-util:0.7.11 The following copyrights and licenses were found in the source code of this package: @@ -31774,7 +32078,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-targets:0.52.5 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32232,7 +32536,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_gnullvm:0.52.5 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32690,7 +32994,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.5 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33148,7 +33452,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.5 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33377,7 +33681,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.5 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33835,7 +34139,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.5 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34293,7 +34597,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.5 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34751,7 +35055,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.52.5 +Package: windows_x86_64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35209,7 +35513,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.52.5 +Package: windows_x86_64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35438,7 +35742,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.7.32 +Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -35690,7 +35994,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.7.32 +Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -35942,7 +36246,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zeroize:1.7.0 +Package: zeroize:1.8.1 The following copyrights and licenses were found in the source code of this package: @@ -36171,25 +36475,2718 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: array-buffer-byte-length:1.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: array-includes:3.1.8 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: array.prototype.findlastindex:1.2.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: array.prototype.flat:1.3.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: array.prototype.flatmap:1.3.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: arraybuffer.prototype.slice:1.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: available-typed-arrays:1.0.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: balanced-match:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: brace-expansion:1.1.11 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: call-bind:1.0.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: child_process:1.0.2 The following copyrights and licenses were found in the source code of this package: -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +---- + +Package: concat-map:0.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: data-view-buffer:1.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: data-view-byte-length:1.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: data-view-byte-offset:1.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: debug:3.2.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: define-data-property:1.1.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: define-properties:1.2.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: detect-libc:2.0.3 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: doctrine:2.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: es-abstract:1.23.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: es-define-property:1.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: es-errors:1.3.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: es-object-atoms:1.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: es-set-tostringtag:2.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: es-shim-unscopables:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: es-to-primitive:1.2.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: eslint-config-prettier:9.1.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: eslint-import-resolver-node:0.3.9 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: eslint-module-utils:2.8.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: eslint-plugin-import:2.29.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: esutils:2.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: find-free-port:2.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +---- + +Package: find-free-ports:3.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: for-each:0.3.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: function-bind:1.1.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: function.prototype.name:1.1.6 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: functions-have-names:1.2.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: get-intrinsic:1.2.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: get-symbol-description:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: globalthis:1.0.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: gopd:1.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: has-bigints:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: has-property-descriptors:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: has-proto:1.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: has-symbols:1.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: has-tostringtag:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: hasown:2.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: internal-slot:1.0.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-array-buffer:3.0.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-bigint:1.0.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-boolean-object:1.1.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-callable:1.2.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-core-module:2.14.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-data-view:1.0.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-date-object:1.0.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-extglob:2.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-glob:4.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-negative-zero:2.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-number-object:1.0.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-regex:1.1.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-shared-array-buffer:1.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-string:1.0.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-symbol:1.0.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-typed-array:1.1.13 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: is-weakref:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: isarray:2.0.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: json5:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: long:5.2.3 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: minimatch:3.1.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +---- + +Package: minimist:1.2.8 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: ms:2.1.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: npmignore:0.3.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object-inspect:1.13.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object-keys:1.1.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object.assign:4.1.5 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object.fromentries:2.0.8 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object.groupby:1.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: object.values:1.2.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: path-parse:1.0.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: possible-typed-array-names:1.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: prettier:3.3.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: protobufjs:7.3.2 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: regexp.prototype.flags:1.5.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: resolve:1.22.8 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: safe-array-concat:1.1.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -THIS SOFTWARE. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: find-free-port:2.0.0 +Package: safe-regex-test:1.0.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: semver:6.3.1 The following copyrights and licenses were found in the source code of this package: @@ -36207,7 +39204,7 @@ THIS SOFTWARE. ---- -Package: find-free-ports:3.1.1 +Package: set-function-length:1.2.2 The following copyrights and licenses were found in the source code of this package: @@ -36232,215 +39229,157 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: long:5.2.3 +Package: set-function-name:2.0.2 The following copyrights and licenses were found in the source code of this package: - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - 1. Definitions. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - "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. +Package: side-channel:1.0.6 - "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. +The following copyrights and licenses were found in the source code of this package: - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - "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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - "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. +Package: string.prototype.trim:1.2.9 - "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." +The following copyrights and licenses were found in the source code of this package: - "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. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - 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. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - 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 +Package: string.prototype.trimend:1.0.8 - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +The following copyrights and licenses were found in the source code of this package: - (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 +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - (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. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - 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. +Package: string.prototype.trimstart:1.0.8 - 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. +The following copyrights and licenses were found in the source code of this package: - 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. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - 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. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - END OF TERMS AND CONDITIONS +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - 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. +Package: strip-bom:3.0.0 - Copyright [yyyy] [name of copyright owner] +The following copyrights and licenses were found in the source code of this package: - 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 +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - 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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: minimist:1.2.8 +Package: supports-preserve-symlinks-flag:1.0.0 The following copyrights and licenses were found in the source code of this package: @@ -36465,7 +39404,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: npmignore:0.3.1 +Package: tsconfig-paths:3.15.0 The following copyrights and licenses were found in the source code of this package: @@ -36490,7 +39429,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: prettier:3.2.5 +Package: typed-array-buffer:1.0.2 The following copyrights and licenses were found in the source code of this package: @@ -36515,34 +39454,103 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobufjs:7.2.6 +Package: typed-array-byte-length:1.0.1 The following copyrights and licenses were found in the source code of this package: -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Neither the name of the ORGANIZATION nor the names of its contributors may be -used to endorse or promote products derived from this software without specific -prior written permission. +---- -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Package: typed-array-byte-offset:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: typed-array-length:1.0.6 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: unbox-primitive:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- @@ -36571,6 +39579,56 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- +Package: which-boxed-primitive:1.0.2 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: which-typed-array:1.1.15 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + Package: @protobufjs:aspromise:1.1.2 The following copyrights and licenses were found in the source code of this package: @@ -36881,7 +39939,32 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: @types:node:20.12.7 +Package: @types:json5:0.0.29 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: @types:node:20.14.11 The following copyrights and licenses were found in the source code of this package: diff --git a/node/hybrid-node-tests/commonjs-test/package.json b/node/hybrid-node-tests/commonjs-test/package.json index 740cff0d29..f45541c135 100644 --- a/node/hybrid-node-tests/commonjs-test/package.json +++ b/node/hybrid-node-tests/commonjs-test/package.json @@ -16,7 +16,7 @@ "dependencies": { "child_process": "^1.0.2", "find-free-port": "^2.0.0", - "glide-for-redis": "file:../../../node/build-ts/cjs" + "valkey-glide": "file:../../../node/build-ts/cjs" }, "devDependencies": { "@types/node": "^18.7.9", diff --git a/node/hybrid-node-tests/ecmascript-test/package.json b/node/hybrid-node-tests/ecmascript-test/package.json index 7aec63ed09..df578128a7 100644 --- a/node/hybrid-node-tests/ecmascript-test/package.json +++ b/node/hybrid-node-tests/ecmascript-test/package.json @@ -16,7 +16,7 @@ "dependencies": { "child_process": "^1.0.2", "find-free-ports": "^3.1.1", - "glide-for-redis": "file:../../../node/build-ts/mjs" + "valkey-glide": "file:../../../node/build-ts/mjs" }, "devDependencies": { "@types/node": "^18.7.9", diff --git a/node/index.ts b/node/index.ts index 832d5e9ea0..e2bac6f555 100644 --- a/node/index.ts +++ b/node/index.ts @@ -1,12 +1,12 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ export { Script } from "glide-rs"; export * from "./src/BaseClient"; export * from "./src/Commands"; export * from "./src/Errors"; +export * from "./src/GlideClient"; +export * from "./src/GlideClusterClient"; export * from "./src/Logger"; -export * from "./src/RedisClient"; -export * from "./src/RedisClusterClient"; export * from "./src/Transaction"; diff --git a/node/npm/glide/.gitignore b/node/npm/glide/.gitignore index 646712d437..36bedddd60 100644 --- a/node/npm/glide/.gitignore +++ b/node/npm/glide/.gitignore @@ -131,3 +131,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +package.json.tmpl diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 4891f8d224..cc181ad00e 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { GLIBC, MUSL, familySync } from "detect-libc"; import { arch, platform } from "process"; let globalObject = global as unknown; @@ -14,10 +15,30 @@ function loadNativeBinding() { case "linux": switch (arch) { case "x64": - nativeBinding = require("@scope/glide-for-redis-linux-x64"); + switch (familySync()) { + case GLIBC: + nativeBinding = require("@scope/valkey-glide-linux-x64"); + break; + case MUSL: + nativeBinding = require("@scope/valkey-glide-linux-musl-x64"); + break; + default: + nativeBinding = require("@scope/valkey-glide-linux-x64"); + break; + } break; case "arm64": - nativeBinding = require("@scope/glide-for-redis-linux-arm64"); + switch (familySync()) { + case GLIBC: + nativeBinding = require("@scope/valkey-glide-linux-arm64"); + break; + case MUSL: + nativeBinding = require("@scope/valkey-glide-linux-musl-arm64"); + break; + default: + nativeBinding = require("@scope/valkey-glide-linux-arm64"); + break; + } break; default: throw new Error( @@ -28,10 +49,10 @@ function loadNativeBinding() { case "darwin": switch (arch) { case "x64": - nativeBinding = require("@scope/glide-for-redis-darwin-x64"); + nativeBinding = require("@scope/valkey-glide-darwin-x64"); break; case "arm64": - nativeBinding = require("@scope/glide-for-redis-darwin-arm64"); + nativeBinding = require("@scope/valkey-glide-darwin-arm64"); break; default: throw new Error( @@ -53,33 +74,95 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { - RedisClient, - RedisClusterClient, + GlideClient, + GlideClusterClient, + GlideClientConfiguration, + SlotIdTypes, + SlotKeyTypes, + RouteByAddress, + Routes, + SingleNodeRoute, + PeriodicChecksManualInterval, + PeriodicChecks, Logger, + LPosOptions, ExpireOptions, InfoOptions, + InsertPosition, + SetOptions, + ZaddOptions, + ScoreBoundry, + RangeByIndex, + RangeByScore, + RangeByLex, + SortedSetRange, + StreamTrimOptions, + StreamAddOptions, + StreamReadOptions, + ScriptOptions, ClosingError, + ConfigurationError, ExecAbortError, RedisError, RequestError, TimeoutError, + ConnectionError, ClusterTransaction, Transaction, + PubSubMsg, + createLeakedArray, + createLeakedAttribute, + createLeakedBigint, + createLeakedDouble, + createLeakedMap, + createLeakedString, + parseInfoResponse, } = nativeBinding; module.exports = { - RedisClient, - RedisClusterClient, + GlideClient, + GlideClusterClient, + GlideClientConfiguration, + SlotIdTypes, + SlotKeyTypes, + RouteByAddress, + Routes, + SingleNodeRoute, + PeriodicChecksManualInterval, + PeriodicChecks, Logger, + LPosOptions, ExpireOptions, InfoOptions, + InsertPosition, + SetOptions, + ZaddOptions, + ScoreBoundry, + RangeByIndex, + RangeByScore, + RangeByLex, + SortedSetRange, + StreamTrimOptions, + StreamAddOptions, + StreamReadOptions, + ScriptOptions, ClosingError, + ConfigurationError, ExecAbortError, RedisError, RequestError, TimeoutError, + ConnectionError, ClusterTransaction, Transaction, + PubSubMsg, + createLeakedArray, + createLeakedAttribute, + createLeakedBigint, + createLeakedDouble, + createLeakedMap, + createLeakedString, + parseInfoResponse, }; globalObject = Object.assign(global, nativeBinding); diff --git a/node/npm/glide/package.json b/node/npm/glide/package.json index b6ded308e1..86a1bb80b0 100644 --- a/node/npm/glide/package.json +++ b/node/npm/glide/package.json @@ -18,7 +18,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/aws/glide-for-redis.git" + "url": "git+https://github.com/valkey-io/valkey-glide.git" }, "keywords": [ "redis", @@ -29,9 +29,9 @@ "author": "Amazon Web Services", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/aws/glide-for-redis/issues" + "url": "https://github.com/valkey-io/valkey-glide/issues" }, - "homepage": "https://github.com/aws/glide-for-redis#readme", + "homepage": "https://github.com/valkey-io/valkey-glide#readme", "devDependencies": { "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.48.0", @@ -40,10 +40,12 @@ "typescript": "^4.9.4" }, "optionalDependencies": { - "${scope}glide-for-redis-darwin-arm64": "${package_version}", - "${scope}glide-for-redis-darwin-x64": "${package_version}", - "${scope}glide-for-redis-linux-arm64": "${package_version}", - "${scope}glide-for-redis-linux-x64": "${package_version}" + "${scope}valkey-glide-darwin-arm64": "${package_version}", + "${scope}valkey-glide-darwin-x64": "${package_version}", + "${scope}valkey-glide-linux-arm64": "${package_version}", + "${scope}valkey-glide-linux-x64": "${package_version}", + "${scope}valkey-glide-linux-musl-arm64": "${package_version}", + "${scope}valkey-glide-linux-musl-x64": "${package_version}" }, "eslintConfig": { "extends": [ @@ -58,5 +60,8 @@ "build-ts/*" ], "root": true + }, + "dependencies": { + "detect-libc": "^2.0.3" } } diff --git a/node/package.json b/node/package.json index 817e6e138f..21a02e70b9 100644 --- a/node/package.json +++ b/node/package.json @@ -1,5 +1,5 @@ { - "name": "@aws/glide-for-redis", + "name": "@valkey/valkey-glide", "description": "An AWS-sponsored, open-source Redis client.", "main": "build-ts/index.js", "module": "build-ts/index.js", @@ -7,14 +7,16 @@ "type": "commonjs", "repository": { "type": "git", - "url": "git+https://github.com/aws/glide-for-redis.git" + "url": "git+https://github.com/valkey-io/valkey-glide.git" }, - "homepage": "https://github.com/aws/glide-for-redis#readme", + "homepage": "https://github.com/valkey-io/valkey-glide#readme", "dependencies": { + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", "glide-rs": "file:rust-client", "long": "^5.2.3", "npmignore": "^0.3.0", - "prettier": "^3.2.5", + "prettier": "^3.3.2", "protobufjs": "^7.2.2" }, "bundleDependencies": [ @@ -32,10 +34,10 @@ "build-protobuf": "npm run compile-protobuf-files && npm run fix-protobuf-file", "compile-protobuf-files": "cd src && pbjs -t static-module -o ProtobufMessage.js ../../glide-core/src/protobuf/*.proto && pbts -o ProtobufMessage.d.ts ProtobufMessage.js", "fix-protobuf-file": "replace 'this\\.encode\\(message, writer\\)\\.ldelim' 'this.encode(message, writer && writer.len ? writer.fork() : writer).ldelim' src/ProtobufMessage.js", - "test": "jest --verbose --runInBand --testPathIgnorePatterns='RedisModules'", + "test": "npm run build-test-utils && jest --verbose --runInBand --testPathIgnorePatterns='RedisModules'", + "build-test-utils": "cd ../utils && npm i && npm run build", "lint": "eslint -f unix \"src/**/*.{ts,tsx}\"", "prepack": "npmignore --auto", - "test-modules": "jest --verbose --runInBand 'tests/RedisModules.test.ts'", "prettier:check:ci": "./node_modules/.bin/prettier --check . --ignore-unknown '!**/*.{js,d.ts}'", "prettier:format": "./node_modules/.bin/prettier --write . --ignore-unknown '!**/*.{js,d.ts}'" }, @@ -43,12 +45,13 @@ "@babel/preset-env": "^7.20.2", "@jest/globals": "^29.5.0", "@types/jest": "^29.4.0", + "@types/minimist": "^1.2.5", "@types/redis-server": "^1.2.0", "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^5.54.1", - "@typescript-eslint/parser": "^5.54.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "babel-jest": "^28.1.3", - "eslint": "^8.36.0", + "eslint": "^8.57.0", "eslint-plugin-tsdoc": "^0.2.17", "find-free-port": "^2.0.0", "jest": "^28.1.3", diff --git a/node/rust-client/Cargo.toml b/node/rust-client/Cargo.toml index 0d9c99c95b..e9e2af8851 100644 --- a/node/rust-client/Cargo.toml +++ b/node/rust-client/Cargo.toml @@ -3,7 +3,7 @@ name = "glide-rs" version = "0.1.0" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,6 +18,7 @@ napi = {version = "2.14", features = ["napi4", "napi6"] } napi-derive = "2.14" logger_core = {path = "../../logger_core"} byteorder = "1.4.3" +bytes = "1" num-traits = "0.2.17" num-bigint = { version = "0.4.4", optional = true } [target.'cfg(not(target_env = "msvc"))'.dependencies] diff --git a/node/rust-client/build.rs b/node/rust-client/build.rs index 7295877eb8..af38e8a35e 100644 --- a/node/rust-client/build.rs +++ b/node/rust-client/build.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ extern crate napi_build; diff --git a/node/rust-client/src/lib.rs b/node/rust-client/src/lib.rs index 3b8c1fd384..b5ea8f39c2 100644 --- a/node/rust-client/src/lib.rs +++ b/node/rust-client/src/lib.rs @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ #[cfg(not(target_env = "msvc"))] @@ -10,10 +10,13 @@ use tikv_jemallocator::Jemalloc; static GLOBAL: Jemalloc = Jemalloc; use byteorder::{LittleEndian, WriteBytesExt}; +use bytes::Bytes; use glide_core::start_socket_listener; use glide_core::MAX_REQUEST_ARGS_LENGTH; #[cfg(feature = "testing_utilities")] use napi::bindgen_prelude::BigInt; +use napi::bindgen_prelude::Either; +use napi::bindgen_prelude::Uint8Array; use napi::{Env, Error, JsObject, JsUnknown, Result, Status}; use napi_derive::napi; use num_traits::sign::Signed; @@ -65,7 +68,8 @@ impl AsyncClient { .build()?; let _runtime_handle = runtime.enter(); let client = to_js_result(redis::Client::open(connection_address))?; - let connection = to_js_result(runtime.block_on(client.get_multiplexed_async_connection()))?; + let connection = + to_js_result(runtime.block_on(client.get_multiplexed_async_connection(None)))?; Ok(AsyncClient { connection, runtime, @@ -159,15 +163,12 @@ pub fn init(level: Option, file_name: Option<&str>) -> Level { fn redis_value_to_js(val: Value, js_env: Env) -> Result { match val { Value::Nil => js_env.get_null().map(|val| val.into_unknown()), - Value::SimpleString(str) => js_env - .create_string_from_std(str) - .map(|val| val.into_unknown()), + Value::SimpleString(str) => Ok(js_env + .create_buffer_with_data(str.as_bytes().to_vec())? + .into_unknown()), Value::Okay => js_env.create_string("OK").map(|val| val.into_unknown()), Value::Int(num) => js_env.create_int64(num).map(|val| val.into_unknown()), - Value::BulkString(data) => { - let str = to_js_result(std::str::from_utf8(data.as_ref()))?; - js_env.create_string(str).map(|val| val.into_unknown()) - } + Value::BulkString(data) => Ok(js_env.create_buffer_with_data(data)?.into_unknown()), Value::Array(array) => { let mut js_array_view = js_env.create_array_with_length(array.len())?; for (index, item) in array.into_iter().enumerate() { @@ -190,9 +191,12 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { // "Normal client libraries may ignore completely the difference between this" // "type and the String type, and return a string in both cases."" // https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md - Value::VerbatimString { format: _, text } => js_env - .create_string_from_std(text) - .map(|val| val.into_unknown()), + Value::VerbatimString { format: _, text } => { + // VerbatimString is binary safe -> convert it into such + Ok(js_env + .create_buffer_with_data(text.as_bytes().to_vec())? + .into_unknown()) + } Value::BigNumber(num) => { let sign = num.is_negative(); let words = num.iter_u64_digits().collect(); @@ -218,11 +222,22 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { Ok(obj.into_unknown()) } - Value::Push { kind: _, data: _ } => todo!(), + Value::Push { kind, data } => { + let mut obj = js_env.create_object()?; + obj.set_named_property("kind", format!("{kind:?}"))?; + let js_array_view = data + .into_iter() + .map(|item| redis_value_to_js(item, js_env)) + .collect::, _>>()?; + obj.set_named_property("values", js_array_view)?; + Ok(obj.into_unknown()) + } } } -#[napi(ts_return_type = "null | string | number | {} | Boolean | BigInt | Set | any[]")] +#[napi( + ts_return_type = "null | string | Uint8Array | number | {} | Boolean | BigInt | Set | any[]" +)] pub fn value_from_split_pointer(js_env: Env, high_bits: u32, low_bits: u32) -> Result { let mut bytes = [0_u8; 8]; (&mut bytes[..4]) @@ -256,8 +271,10 @@ pub fn create_leaked_string(message: String) -> [u32; 2] { } #[napi(ts_return_type = "[number, number]")] -pub fn create_leaked_string_vec(message: Vec) -> [u32; 2] { - let pointer = Box::leak(Box::new(message)) as *mut Vec; +pub fn create_leaked_string_vec(message: Vec) -> [u32; 2] { + // Convert the string vec -> Bytes vector + let bytes_vec: Vec = message.iter().map(|v| Bytes::from(v.to_vec())).collect(); + let pointer = Box::leak(Box::new(bytes_vec)) as *mut Vec; split_pointer(pointer) } @@ -343,8 +360,11 @@ impl Script { /// Construct with the script's code. #[napi(constructor)] #[allow(dead_code)] - pub fn new(code: String) -> Self { - let hash = glide_core::scripts_container::add_script(&code); + pub fn new(code: Either) -> Self { + let hash = match code { + Either::A(code_str) => glide_core::scripts_container::add_script(code_str.as_bytes()), + Either::B(code_bytes) => glide_core::scripts_container::add_script(&code_bytes), + }; Self { hash } } diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index c2a36c8b96..5af5ce1ade 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { @@ -10,8 +10,12 @@ import { } from "glide-rs"; import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; +import { LPosOptions } from "./command-options/LPosOptions"; import { + AggregationType, ExpireOptions, + InsertPosition, + KeyWeight, RangeByIndex, RangeByLex, RangeByScore, @@ -20,8 +24,9 @@ import { StreamAddOptions, StreamReadOptions, StreamTrimOptions, - ZaddOptions, - createBrpop, + ZAddOptions, + createBLPop, + createBRPop, createDecr, createDecrBy, createDel, @@ -39,63 +44,94 @@ import { createHMGet, createHSet, createHSetNX, - createHvals, + createHVals, createIncr, createIncrBy, createIncrByFloat, + createLIndex, + createLInsert, createLLen, createLPop, + createLPos, createLPush, + createLPushX, createLRange, createLRem, + createLSet, createLTrim, - createLindex, createMGet, createMSet, + createObjectEncoding, + createObjectFreq, + createObjectIdletime, + createObjectRefcount, createPExpire, createPExpireAt, + createPTTL, createPersist, - createPttl, + createPfAdd, + createPfCount, createRPop, createRPush, + createRPushX, createRename, + createRenameNX, createSAdd, createSCard, + createSDiff, + createSDiffStore, + createSInter, + createSInterCard, + createSInterStore, + createSIsMember, + createSMIsMember, createSMembers, + createSMove, createSPop, createSRem, + createSUnion, + createSUnionStore, createSet, - createSismember, createStrlen, createTTL, createType, createUnlink, - createXadd, - createXread, - createXtrim, - createZadd, - createZcard, - createZcount, - createZpopmax, - createZpopmin, - createZrange, - createZrangeWithScores, - createZrank, - createZrem, - createZremRangeByRank, - createZremRangeByScore, - createZscore, + createXAdd, + createXLen, + createXRead, + createXTrim, + createZAdd, + createZCard, + createZCount, + createZInterCard, + createZInterstore, + createZPopMax, + createZPopMin, + createZRange, + createZRangeWithScores, + createZRank, + createZRem, + createZRemRangeByRank, + createZRemRangeByScore, + createZScore, } from "./Commands"; import { ClosingError, + ConfigurationError, ConnectionError, ExecAbortError, RedisError, RequestError, TimeoutError, } from "./Errors"; +import { GlideClientConfiguration } from "./GlideClient"; +import { ClusterClientConfiguration } from "./GlideClusterClient"; import { Logger } from "./Logger"; -import { connection_request, redis_request, response } from "./ProtobufMessage"; +import { + command_request, + connection_request, + response, +} from "./ProtobufMessage"; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ type PromiseFunction = (value?: any) => void; @@ -147,14 +183,16 @@ export type BaseClientConfiguration = { * DNS Addresses and ports of known nodes in the cluster. * If the server is in cluster mode the list can be partial, as the client will attempt to map out the cluster and find all nodes. * If the server is in standalone mode, only nodes whose addresses were provided will be used by the client. + * * @example - * + * ```typescript + * configuration.addresses = * [ - * \{ address:sample-address-0001.use1.cache.amazonaws.com, port:6378 \}, - * \{ address: sample-address-0002.use2.cache.amazonaws.com \} - * \{ address: sample-address-0003.use2.cache.amazonaws.com, port:6380 \} + * { address: sample-address-0001.use1.cache.amazonaws.com, port:6378 }, + * { address: sample-address-0002.use2.cache.amazonaws.com } + * { address: sample-address-0003.use2.cache.amazonaws.com, port:6380 } * ] - * + * ``` */ addresses: { host: string; @@ -202,11 +240,11 @@ export type ScriptOptions = { /** * The keys that are used in the script. */ - keys?: string[]; + keys?: (string | Uint8Array)[]; /** * The arguments for the script. */ - args?: string[]; + args?: (string | Uint8Array)[]; }; function getRequestErrorClass( @@ -231,6 +269,11 @@ function getRequestErrorClass( return RequestError; } +export type PubSubMsg = { + message: string; + channel: string; + pattern?: string | null; +}; export class BaseClient { private socket: net.Socket; private readonly promiseCallbackFunctions: [ @@ -243,7 +286,54 @@ export class BaseClient { private remainingReadData: Uint8Array | undefined; private readonly requestTimeout: number; // Timeout in milliseconds private isClosed = false; + private readonly pubsubFutures: [PromiseFunction, ErrorFunction][] = []; + private pendingPushNotification: response.Response[] = []; + private config: BaseClientConfiguration | undefined; + + protected configurePubsub( + options: ClusterClientConfiguration | GlideClientConfiguration, + configuration: connection_request.IConnectionRequest, + ) { + if (options.pubsubSubscriptions) { + if (options.protocol == ProtocolVersion.RESP2) { + throw new ConfigurationError( + "PubSub subscriptions require RESP3 protocol, but RESP2 was configured.", + ); + } + const { context, callback } = options.pubsubSubscriptions; + + if (context && !callback) { + throw new ConfigurationError( + "PubSub subscriptions with a context require a callback function to be configured.", + ); + } + + configuration.pubsubSubscriptions = + connection_request.PubSubSubscriptions.create({}); + + for (const [channelType, channelsPatterns] of Object.entries( + options.pubsubSubscriptions.channelsAndPatterns, + )) { + let entry = + configuration.pubsubSubscriptions! + .channelsOrPatternsByType![parseInt(channelType)]; + + if (!entry) { + entry = connection_request.PubSubChannelsOrPatterns.create({ + channelsOrPatterns: [], + }); + configuration.pubsubSubscriptions!.channelsOrPatternsByType![ + parseInt(channelType) + ] = entry; + } + + for (const channelPattern of channelsPatterns) { + entry.channelsOrPatterns!.push(Buffer.from(channelPattern)); + } + } + } + } private handleReadData(data: Buffer) { const buf = this.remainingReadData ? Buffer.concat([this.remainingReadData, data]) @@ -271,40 +361,69 @@ export class BaseClient { } } - if (message.closingError != null) { - this.close(message.closingError); - return; + if (message.isPush) { + this.processPush(message); + } else { + this.processResponse(message); } + } - const [resolve, reject] = - this.promiseCallbackFunctions[message.callbackIdx]; - this.availableCallbackSlots.push(message.callbackIdx); + this.remainingReadData = undefined; + } - if (message.requestError != null) { - const errorType = getRequestErrorClass( - message.requestError.type, - ); - reject( - new errorType(message.requestError.message ?? undefined), - ); - } else if (message.respPointer != null) { - const pointer = message.respPointer; + processResponse(message: response.Response) { + if (message.closingError != null) { + this.close(message.closingError); + return; + } - if (typeof pointer === "number") { - resolve(valueFromSplitPointer(0, pointer)); - } else { - resolve(valueFromSplitPointer(pointer.high, pointer.low)); - } - } else if ( - message.constantResponse === response.ConstantResponse.OK - ) { - resolve("OK"); + const [resolve, reject] = + this.promiseCallbackFunctions[message.callbackIdx]; + this.availableCallbackSlots.push(message.callbackIdx); + + if (message.requestError != null) { + const errorType = getRequestErrorClass(message.requestError.type); + reject(new errorType(message.requestError.message ?? undefined)); + } else if (message.respPointer != null) { + const pointer = message.respPointer; + + if (typeof pointer === "number") { + resolve(valueFromSplitPointer(0, pointer)); } else { - resolve(null); + resolve(valueFromSplitPointer(pointer.high, pointer.low)); } + } else if (message.constantResponse === response.ConstantResponse.OK) { + resolve("OK"); + } else { + resolve(null); } + } - this.remainingReadData = undefined; + processPush(response: response.Response) { + if (response.closingError != null || !response.respPointer) { + const errMsg = response.closingError + ? response.closingError + : "Client Error - push notification without resp_pointer"; + + this.close(errMsg); + return; + } + + const [callback, context] = this.getPubsubCallbackAndContext( + this.config!, + ); + + if (callback) { + const pubsubMessage = + this.notificationToPubSubMessageSafe(response); + + if (pubsubMessage) { + callback(pubsubMessage, context); + } + } else { + this.pendingPushNotification.push(response); + this.completePubSubFuturesSafe(); + } } /** @@ -316,6 +435,7 @@ export class BaseClient { ) { // if logger has been initialized by the external-user on info level this log will be shown Logger.log("info", "Client lifetime", `construct client`); + this.config = options; this.requestTimeout = options?.requestTimeout ?? DEFAULT_TIMEOUT_IN_MILLISECONDS; this.socket = socket; @@ -353,10 +473,10 @@ export class BaseClient { */ protected createWritePromise( command: - | redis_request.Command - | redis_request.Command[] - | redis_request.ScriptInvocation, - route?: redis_request.Routes, + | command_request.Command + | command_request.Command[] + | command_request.ScriptInvocation, + route?: command_request.Routes, ): Promise { if (this.isClosed) { throw new ClosingError( @@ -367,31 +487,31 @@ export class BaseClient { return new Promise((resolve, reject) => { const callbackIndex = this.getCallbackIndex(); this.promiseCallbackFunctions[callbackIndex] = [resolve, reject]; - this.writeOrBufferRedisRequest(callbackIndex, command, route); + this.writeOrBufferCommandRequest(callbackIndex, command, route); }); } - private writeOrBufferRedisRequest( + private writeOrBufferCommandRequest( callbackIdx: number, command: - | redis_request.Command - | redis_request.Command[] - | redis_request.ScriptInvocation, - route?: redis_request.Routes, + | command_request.Command + | command_request.Command[] + | command_request.ScriptInvocation, + route?: command_request.Routes, ) { const message = Array.isArray(command) - ? redis_request.RedisRequest.create({ + ? command_request.CommandRequest.create({ callbackIdx, - transaction: redis_request.Transaction.create({ + transaction: command_request.Transaction.create({ commands: command, }), }) - : command instanceof redis_request.Command - ? redis_request.RedisRequest.create({ + : command instanceof command_request.Command + ? command_request.CommandRequest.create({ callbackIdx, singleCommand: command, }) - : redis_request.RedisRequest.create({ + : command_request.CommandRequest.create({ callbackIdx, scriptInvocation: command, }); @@ -399,8 +519,8 @@ export class BaseClient { this.writeOrBufferRequest( message, - (message: redis_request.RedisRequest, writer: Writer) => { - redis_request.RedisRequest.encodeDelimited(message, writer); + (message: command_request.CommandRequest, writer: Writer) => { + command_request.CommandRequest.encodeDelimited(message, writer); }, ); } @@ -418,8 +538,196 @@ export class BaseClient { this.writeBufferedRequestsToSocket(); } + // Define a common function to process the result of a transaction with set commands + /** + * @internal + */ + protected processResultWithSetCommands( + result: ReturnType[] | null, + setCommandsIndexes: number[], + ): ReturnType[] | null { + if (result === null) { + return null; + } + + for (const index of setCommandsIndexes) { + result[index] = new Set(result[index] as ReturnType[]); + } + + return result; + } + + cancelPubSubFuturesWithExceptionSafe(exception: ConnectionError): void { + while (this.pubsubFutures.length > 0) { + const nextFuture = this.pubsubFutures.shift(); + + if (nextFuture) { + const [, reject] = nextFuture; + reject(exception); + } + } + } + + isPubsubConfigured( + config: GlideClientConfiguration | ClusterClientConfiguration, + ): boolean { + return !!config.pubsubSubscriptions; + } + + getPubsubCallbackAndContext( + config: GlideClientConfiguration | ClusterClientConfiguration, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ): [((msg: PubSubMsg, context: any) => void) | null | undefined, any] { + if (config.pubsubSubscriptions) { + return [ + config.pubsubSubscriptions.callback, + config.pubsubSubscriptions.context, + ]; + } + + return [null, null]; + } + + public getPubSubMessage(): Promise { + if (this.isClosed) { + throw new ClosingError( + "Unable to execute requests; the client is closed. Please create a new client.", + ); + } + + if (!this.isPubsubConfigured(this.config!)) { + throw new ConfigurationError( + "The operation will never complete since there was no pubsbub subscriptions applied to the client.", + ); + } + + if (this.getPubsubCallbackAndContext(this.config!)[0]) { + throw new ConfigurationError( + "The operation will never complete since messages will be passed to the configured callback.", + ); + } + + return new Promise((resolve, reject) => { + this.pubsubFutures.push([resolve, reject]); + this.completePubSubFuturesSafe(); + }); + } + + public tryGetPubSubMessage(): PubSubMsg | null { + if (this.isClosed) { + throw new ClosingError( + "Unable to execute requests; the client is closed. Please create a new client.", + ); + } + + if (!this.isPubsubConfigured(this.config!)) { + throw new ConfigurationError( + "The operation will never complete since there was no pubsbub subscriptions applied to the client.", + ); + } + + if (this.getPubsubCallbackAndContext(this.config!)[0]) { + throw new ConfigurationError( + "The operation will never complete since messages will be passed to the configured callback.", + ); + } + + let msg: PubSubMsg | null = null; + this.completePubSubFuturesSafe(); + + while (this.pendingPushNotification.length > 0 && !msg) { + const pushNotification = this.pendingPushNotification.shift()!; + msg = this.notificationToPubSubMessageSafe(pushNotification); + } + + return msg; + } + notificationToPubSubMessageSafe( + pushNotification: response.Response, + ): PubSubMsg | null { + let msg: PubSubMsg | null = null; + const responsePointer = pushNotification.respPointer; + let nextPushNotificationValue: Record = {}; + + if (responsePointer) { + if (typeof responsePointer !== "number") { + nextPushNotificationValue = valueFromSplitPointer( + responsePointer.high, + responsePointer.low, + ) as Record; + } else { + nextPushNotificationValue = valueFromSplitPointer( + 0, + responsePointer, + ) as Record; + } + + const messageKind = nextPushNotificationValue["kind"]; + + if (messageKind === "Disconnect") { + Logger.log( + "warn", + "disconnect notification", + "Transport disconnected, messages might be lost", + ); + } else if ( + messageKind === "Message" || + messageKind === "PMessage" || + messageKind === "SMessage" + ) { + const values = nextPushNotificationValue["values"] as string[]; + + if (messageKind === "PMessage") { + msg = { + message: values[2], + channel: values[1], + pattern: values[0], + }; + } else { + msg = { + message: values[1], + channel: values[0], + pattern: null, + }; + } + } else if ( + messageKind === "PSubscribe" || + messageKind === "Subscribe" || + messageKind === "SSubscribe" || + messageKind === "Unsubscribe" || + messageKind === "SUnsubscribe" || + messageKind === "PUnsubscribe" + ) { + // pass + } else { + Logger.log( + "error", + "unknown notification", + `Unknown notification: '${messageKind}'`, + ); + } + } + + return msg; + } + completePubSubFuturesSafe() { + while ( + this.pendingPushNotification.length > 0 && + this.pubsubFutures.length > 0 + ) { + const nextPushNotification = this.pendingPushNotification.shift()!; + const pubsubMessage = + this.notificationToPubSubMessageSafe(nextPushNotification); + + if (pubsubMessage) { + const [resolve] = this.pubsubFutures.shift()!; + resolve(pubsubMessage); + } + } + } + /** Get the value associated with the given key, or null if no such value exists. - * See https://redis.io/commands/get/ for details. + * See https://valkey.io/commands/get/ for details. * * @param key - The key to retrieve from the database. * @returns If `key` exists, returns the value of `key` as a string. Otherwise, return null. @@ -436,7 +744,7 @@ export class BaseClient { } /** Set the given key with the given value. Return value is dependent on the passed options. - * See https://redis.io/commands/set/ for details. + * See https://valkey.io/commands/set/ for details. * * @param key - The key to store. * @param value - The value to store with the given key. @@ -465,15 +773,15 @@ export class BaseClient { * ``` */ public set( - key: string, - value: string, + key: string | Uint8Array, + value: string | Uint8Array, options?: SetOptions, ): Promise<"OK" | string | null> { return this.createWritePromise(createSet(key, value, options)); } /** Removes the specified keys. A key is ignored if it does not exist. - * See https://redis.io/commands/del/ for details. + * See https://valkey.io/commands/del/ for details. * * @param keys - the keys we wanted to remove. * @returns the number of keys that were removed. @@ -498,8 +806,9 @@ export class BaseClient { } /** Retrieve the values of multiple keys. - * See https://redis.io/commands/mget/ for details. + * See https://valkey.io/commands/mget/ for details. * + * @remarks When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. * @param keys - A list of keys to retrieve values for. * @returns A list of values corresponding to the provided keys. If a key is not found, * its corresponding value in the list will be null. @@ -518,8 +827,9 @@ export class BaseClient { } /** Set multiple keys to multiple values in a single operation. - * See https://redis.io/commands/mset/ for details. + * See https://valkey.io/commands/mset/ for details. * + * @remarks When in cluster mode, the command may route to multiple nodes when keys in `keyValueMap` map to different hash slots. * @param keyValueMap - A key-value map consisting of keys and their respective values to set. * @returns always "OK". * @@ -535,7 +845,7 @@ export class BaseClient { } /** Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/incr/ for details. + * See https://valkey.io/commands/incr/ for details. * * @param key - The key to increment its value. * @returns the value of `key` after the increment. @@ -553,7 +863,7 @@ export class BaseClient { } /** Increments the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/incrby/ for details. + * See https://valkey.io/commands/incrby/ for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -574,7 +884,7 @@ export class BaseClient { /** Increment the string representing a floating point number stored at `key` by `amount`. * By using a negative increment value, the result is that the value stored at `key` is decremented. * If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/incrbyfloat/ for details. + * See https://valkey.io/commands/incrbyfloat/ for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -593,7 +903,7 @@ export class BaseClient { } /** Decrements the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/decr/ for details. + * See https://valkey.io/commands/decr/ for details. * * @param key - The key to decrement its value. * @returns the value of `key` after the decrement. @@ -611,7 +921,7 @@ export class BaseClient { } /** Decrements the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/decrby/ for details. + * See https://valkey.io/commands/decrby/ for details. * * @param key - The key to decrement its value. * @param amount - The amount to decrement. @@ -630,7 +940,7 @@ export class BaseClient { } /** Retrieve the value associated with `field` in the hash stored at `key`. - * See https://redis.io/commands/hget/ for details. + * See https://valkey.io/commands/hget/ for details. * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. @@ -656,7 +966,7 @@ export class BaseClient { } /** Sets the specified fields to their respective values in the hash stored at `key`. - * See https://redis.io/commands/hset/ for details. + * See https://valkey.io/commands/hset/ for details. * * @param key - The key of the hash. * @param fieldValueMap - A field-value map consisting of fields and their corresponding values @@ -680,7 +990,7 @@ export class BaseClient { /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. * If `key` does not exist, a new key holding a hash is created. * If `field` already exists, this operation has no effect. - * See https://redis.io/commands/hsetnx/ for more details. + * See https://valkey.io/commands/hsetnx/ for more details. * * @param key - The key of the hash. * @param field - The field to set the value for. @@ -707,7 +1017,7 @@ export class BaseClient { /** Removes the specified fields from the hash stored at `key`. * Specified fields that do not exist within this hash are ignored. - * See https://redis.io/commands/hdel/ for details. + * See https://valkey.io/commands/hdel/ for details. * * @param key - The key of the hash. * @param fields - The fields to remove from the hash stored at `key`. @@ -726,7 +1036,7 @@ export class BaseClient { } /** Returns the values associated with the specified fields in the hash stored at `key`. - * See https://redis.io/commands/hmget/ for details. + * See https://valkey.io/commands/hmget/ for details. * * @param key - The key of the hash. * @param fields - The fields in the hash stored at `key` to retrieve from the database. @@ -746,7 +1056,7 @@ export class BaseClient { } /** Returns if `field` is an existing field in the hash stored at `key`. - * See https://redis.io/commands/hexists/ for details. + * See https://valkey.io/commands/hexists/ for details. * * @param key - The key of the hash. * @param field - The field to check in the hash stored at `key`. @@ -771,7 +1081,7 @@ export class BaseClient { } /** Returns all fields and values of the hash stored at `key`. - * See https://redis.io/commands/hgetall/ for details. + * See https://valkey.io/commands/hgetall/ for details. * * @param key - The key of the hash. * @returns a list of fields and their values stored in the hash. Every field name in the list is followed by its value. @@ -791,7 +1101,7 @@ export class BaseClient { /** Increments the number stored at `field` in the hash stored at `key` by increment. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/hincrby/ for details. + * See https://valkey.io/commands/hincrby/ for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -816,7 +1126,7 @@ export class BaseClient { /** Increment the string representing a floating point number stored at `field` in the hash stored at `key` by increment. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/hincrbyfloat/ for details. + * See https://valkey.io/commands/hincrbyfloat/ for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -839,7 +1149,7 @@ export class BaseClient { } /** Returns the number of fields contained in the hash stored at `key`. - * See https://redis.io/commands/hlen/ for more details. + * See https://valkey.io/commands/hlen/ for more details. * * @param key - The key of the hash. * @returns The number of fields in the hash, or 0 when the key does not exist. @@ -863,7 +1173,7 @@ export class BaseClient { } /** Returns all values in the hash stored at key. - * See https://redis.io/commands/hvals/ for more details. + * See https://valkey.io/commands/hvals/ for more details. * * @param key - The key of the hash. * @returns a list of values in the hash, or an empty list when the key does not exist. @@ -876,13 +1186,13 @@ export class BaseClient { * ``` */ public hvals(key: string): Promise { - return this.createWritePromise(createHvals(key)); + return this.createWritePromise(createHVals(key)); } /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://redis.io/commands/lpush/ for details. + * See https://valkey.io/commands/lpush/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. @@ -906,9 +1216,28 @@ export class BaseClient { return this.createWritePromise(createLPush(key, elements)); } + /** + * Inserts specified values at the head of the `list`, only if `key` already + * exists and holds a list. + * + * See https://valkey.io/commands/lpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the head of the list stored at `key`. + * @returns The length of the list after the push operation. + * @example + * ```typescript + * const listLength = await client.lpushx("my_list", ["value1", "value2"]); + * console.log(result); // Output: 2 - Indicates that the list has two elements. + * ``` + */ + public lpushx(key: string, elements: string[]): Promise { + return this.createWritePromise(createLPushX(key, elements)); + } + /** Removes and returns the first elements of the list stored at `key`. * The command pops a single element from the beginning of the list. - * See https://redis.io/commands/lpop/ for details. + * See https://valkey.io/commands/lpop/ for details. * * @param key - The key of the list. * @returns The value of the first element. @@ -933,7 +1262,7 @@ export class BaseClient { } /** Removes and returns up to `count` elements of the list stored at `key`, depending on the list's length. - * See https://redis.io/commands/lpop/ for details. + * See https://valkey.io/commands/lpop/ for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -962,7 +1291,7 @@ export class BaseClient { * The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. * These offsets can also be negative numbers indicating offsets starting at the end of the list, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://redis.io/commands/lrange/ for details. + * See https://valkey.io/commands/lrange/ for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -998,7 +1327,7 @@ export class BaseClient { } /** Returns the length of the list stored at `key`. - * See https://redis.io/commands/llen/ for details. + * See https://valkey.io/commands/llen/ for details. * * @param key - The key of the list. * @returns the length of the list at `key`. @@ -1015,11 +1344,35 @@ export class BaseClient { return this.createWritePromise(createLLen(key)); } + /** + * Sets the list element at `index` to `element`. + * The index is zero-based, so `0` means the first element, `1` the second element and so on. + * Negative indices can be used to designate elements starting at the tail of + * the list. Here, `-1` means the last element, `-2` means the penultimate and so forth. + * + * See https://valkey.io/commands/lset/ for details. + * + * @param key - The key of the list. + * @param index - The index of the element in the list to be set. + * @param element - The new element to set at the specified index. + * @returns Always "OK". + * + * @example + * ```typescript + * // Example usage of the lset method + * const response = await client.lset("test_key", 1, "two"); + * console.log(response); // Output: 'OK' - Indicates that the second index of the list has been set to "two". + * ``` + */ + public lset(key: string, index: number, element: string): Promise<"OK"> { + return this.createWritePromise(createLSet(key, index, element)); + } + /** Trim an existing list so that it will contain only the specified range of elements specified. * The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. * These offsets can also be negative numbers indicating offsets starting at the end of the list, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://redis.io/commands/ltrim/ for details. + * See https://valkey.io/commands/ltrim/ for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -1065,7 +1418,7 @@ export class BaseClient { /** Inserts all the specified values at the tail of the list stored at `key`. * `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://redis.io/commands/rpush/ for details. + * See https://valkey.io/commands/rpush/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. @@ -1089,9 +1442,28 @@ export class BaseClient { return this.createWritePromise(createRPush(key, elements)); } + /** + * Inserts specified values at the tail of the `list`, only if `key` already + * exists and holds a list. + * + * See https://valkey.io/commands/rpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the tail of the list stored at `key`. + * @returns The length of the list after the push operation. + * @example + * ```typescript + * const result = await client.rpushx("my_list", ["value1", "value2"]); + * console.log(result); // Output: 2 - Indicates that the list has two elements. + * ``` + * */ + public rpushx(key: string, elements: string[]): Promise { + return this.createWritePromise(createRPushX(key, elements)); + } + /** Removes and returns the last elements of the list stored at `key`. * The command pops a single element from the end of the list. - * See https://redis.io/commands/rpop/ for details. + * See https://valkey.io/commands/rpop/ for details. * * @param key - The key of the list. * @returns The value of the last element. @@ -1116,7 +1488,7 @@ export class BaseClient { } /** Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. - * See https://redis.io/commands/rpop/ for details. + * See https://valkey.io/commands/rpop/ for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -1143,7 +1515,7 @@ export class BaseClient { /** Adds the specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. * If `key` does not exist, a new set is created before adding `members`. - * See https://redis.io/commands/sadd/ for details. + * See https://valkey.io/commands/sadd/ for details. * * @param key - The key to store the members to its set. * @param members - A list of members to add to the set stored at `key`. @@ -1161,7 +1533,7 @@ export class BaseClient { } /** Removes the specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. - * See https://redis.io/commands/srem/ for details. + * See https://valkey.io/commands/srem/ for details. * * @param key - The key to remove the members from its set. * @param members - A list of members to remove from the set stored at `key`. @@ -1180,25 +1552,54 @@ export class BaseClient { } /** Returns all the members of the set value stored at `key`. - * See https://redis.io/commands/smembers/ for details. + * See https://valkey.io/commands/smembers/ for details. * * @param key - The key to return its members. - * @returns All members of the set. - * If `key` does not exist, it is treated as an empty set and this command returns empty list. + * @returns A `Set` containing all members of the set. + * If `key` does not exist, it is treated as an empty set and this command returns an empty `Set`. * * @example * ```typescript * // Example usage of the smembers method * const result = await client.smembers("my_set"); - * console.log(result); // Output: ["member1", "member2", "member3"] + * console.log(result); // Output: Set {'member1', 'member2', 'member3'} + * ``` + */ + public smembers(key: string): Promise> { + return this.createWritePromise(createSMembers(key)).then( + (smembes) => new Set(smembes), + ); + } + + /** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. + * Creates a new destination set if needed. The operation is atomic. + * See https://valkey.io/commands/smove for more details. + * + * @remarks When in cluster mode, `source` and `destination` must map to the same hash slot. + * + * @param source - The key of the set to remove the element from. + * @param destination - The key of the set to add the element to. + * @param member - The set element to move. + * @returns `true` on success, or `false` if the `source` set does not exist or the element is not a member of the source set. + * + * @example + * ```typescript + * const result = await client.smove("set1", "set2", "member1"); + * console.log(result); // Output: true - "member1" was moved from "set1" to "set2". * ``` */ - public smembers(key: string): Promise { - return this.createWritePromise(createSMembers(key)); + public smove( + source: string, + destination: string, + member: string, + ): Promise { + return this.createWritePromise( + createSMove(source, destination, member), + ); } /** Returns the set cardinality (number of elements) of the set stored at `key`. - * See https://redis.io/commands/scard/ for details. + * See https://valkey.io/commands/scard/ for details. * * @param key - The key to return the number of its members. * @returns The cardinality (number of elements) of the set, or 0 if key does not exist. @@ -1214,8 +1615,176 @@ export class BaseClient { return this.createWritePromise(createSCard(key)); } + /** Gets the intersection of all the given sets. + * See https://valkey.io/docs/latest/commands/sinter/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The `keys` of the sets to get the intersection. + * @returns - A set of members which are present in all given sets. + * If one or more sets do not exist, an empty set will be returned. + * + * @example + * ```typescript + * // Example usage of sinter method when member exists + * const result = await client.sinter(["my_set1", "my_set2"]); + * console.log(result); // Output: Set {'member2'} - Indicates that sets have one common member + * ``` + * + * @example + * ```typescript + * // Example usage of sinter method with non-existing key + * const result = await client.sinter(["my_set", "non_existing_key"]); + * console.log(result); // Output: Set {} - An empty set is returned since the key does not exist. + * ``` + */ + public sinter(keys: string[]): Promise> { + return this.createWritePromise(createSInter(keys)).then( + (sinter) => new Set(sinter), + ); + } + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * See https://valkey.io/commands/sintercard/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sets. + * @returns The cardinality of the intersection result. If one or more sets do not exist, `0` is returned. + * + * since Valkey version 7.0.0. + * + * @example + * ```typescript + * await client.sadd("set1", ["a", "b", "c"]); + * await client.sadd("set2", ["b", "c", "d"]); + * const result1 = await client.sintercard(["set1", "set2"]); + * console.log(result1); // Output: 2 - The intersection of "set1" and "set2" contains 2 elements: "b" and "c". + * + * const result2 = await client.sintercard(["set1", "set2"], 1); + * console.log(result2); // Output: 1 - The computation stops early as the intersection cardinality reaches the limit of 1. + * ``` + */ + public sintercard(keys: string[], limit?: number): Promise { + return this.createWritePromise(createSInterCard(keys, limit)); + } + + /** + * Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. + * + * See https://valkey.io/commands/sinterstore/ for more details. + * + * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * @param destination - The key of the destination set. + * @param keys - The keys from which to retrieve the set members. + * @returns The number of elements in the resulting set. + * + * @example + * ```typescript + * const result = await client.sinterstore("my_set", ["set1", "set2"]); + * console.log(result); // Output: 2 - Two elements were stored at "my_set", and those elements are the intersection of "set1" and "set2". + * ``` + */ + public sinterstore(destination: string, keys: string[]): Promise { + return this.createWritePromise(createSInterStore(destination, keys)); + } + + /** + * Computes the difference between the first set and all the successive sets in `keys`. + * + * See https://valkey.io/commands/sdiff/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sets to diff. + * @returns A `Set` of elements representing the difference between the sets. + * If a key in `keys` does not exist, it is treated as an empty set. + * + * @example + * ```typescript + * await client.sadd("set1", ["member1", "member2"]); + * await client.sadd("set2", ["member1"]); + * const result = await client.sdiff(["set1", "set2"]); + * console.log(result); // Output: Set {"member1"} - "member2" is in "set1" but not "set2" + * ``` + */ + public sdiff(keys: string[]): Promise> { + return this.createWritePromise(createSDiff(keys)).then( + (sdiff) => new Set(sdiff), + ); + } + + /** + * Stores the difference between the first set and all the successive sets in `keys` into a new set at `destination`. + * + * See https://valkey.io/commands/sdiffstore/ for more details. + * + * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * @param destination - The key of the destination set. + * @param keys - The keys of the sets to diff. + * @returns The number of elements in the resulting set. + * + * @example + * ```typescript + * await client.sadd("set1", ["member1", "member2"]); + * await client.sadd("set2", ["member1"]); + * const result = await client.sdiffstore("set3", ["set1", "set2"]); + * console.log(result); // Output: 1 - One member was stored in "set3", and that member is the diff between "set1" and "set2". + * ``` + */ + public sdiffstore(destination: string, keys: string[]): Promise { + return this.createWritePromise(createSDiffStore(destination, keys)); + } + + /** + * Gets the union of all the given sets. + * + * See https://valkey.io/commands/sunion/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sets. + * @returns A `Set` of members which are present in at least one of the given sets. + * If none of the sets exist, an empty `Set` will be returned. + * + * @example + * ```typescript + * await client.sadd("my_set1", ["member1", "member2"]); + * await client.sadd("my_set2", ["member2", "member3"]); + * const result1 = await client.sunion(["my_set1", "my_set2"]); + * console.log(result1); // Output: Set {'member1', 'member2', 'member3'} - Sets "my_set1" and "my_set2" have three unique members. + * + * const result2 = await client.sunion(["my_set1", "non_existing_set"]); + * console.log(result2); // Output: Set {'member1', 'member2'} + * ``` + */ + public sunion(keys: string[]): Promise> { + return this.createWritePromise(createSUnion(keys)).then( + (sunion) => new Set(sunion), + ); + } + + /** + * Stores the members of the union of all given sets specified by `keys` into a new set + * at `destination`. + * + * See https://valkey.io/commands/sunionstore/ for details. + * + * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * @param destination - The key of the destination set. + * @param keys - The keys from which to retrieve the set members. + * @returns The number of elements in the resulting set. + * + * @example + * ```typescript + * const length = await client.sunionstore("mySet", ["set1", "set2"]); + * console.log(length); // Output: 2 - Two elements were stored in "mySet", and those two members are the union of "set1" and "set2". + * ``` + */ + public sunionstore(destination: string, keys: string[]): Promise { + return this.createWritePromise(createSUnionStore(destination, keys)); + } + /** Returns if `member` is a member of the set stored at `key`. - * See https://redis.io/commands/sismember/ for more details. + * See https://valkey.io/commands/sismember/ for more details. * * @param key - The key of the set. * @param member - The member to check for existence in the set. @@ -1237,11 +1806,33 @@ export class BaseClient { * ``` */ public sismember(key: string, member: string): Promise { - return this.createWritePromise(createSismember(key, member)); + return this.createWritePromise(createSIsMember(key, member)); + } + + /** + * Checks whether each member is contained in the members of the set stored at `key`. + * + * See https://valkey.io/commands/smismember/ for more details. + * + * @param key - The key of the set to check. + * @param members - A list of members to check for existence in the set. + * @returns An `array` of `boolean` values, each indicating if the respective member exists in the set. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.sadd("set1", ["a", "b", "c"]); + * const result = await client.smismember("set1", ["b", "c", "d"]); + * console.log(result); // Output: [true, true, false] - "b" and "c" are members of "set1", but "d" is not. + * ``` + */ + public smismember(key: string, members: string[]): Promise { + return this.createWritePromise(createSMIsMember(key, members)); } /** Removes and returns one random member from the set value store at `key`. - * See https://redis.io/commands/spop/ for details. + * See https://valkey.io/commands/spop/ for details. * To pop multiple members, see `spopCount`. * * @param key - The key of the set. @@ -1267,31 +1858,35 @@ export class BaseClient { } /** Removes and returns up to `count` random members from the set value store at `key`, depending on the set's length. - * See https://redis.io/commands/spop/ for details. + * See https://valkey.io/commands/spop/ for details. * * @param key - The key of the set. * @param count - The count of the elements to pop from the set. - * @returns A list of popped elements will be returned depending on the set's length. - * If `key` does not exist, empty list will be returned. + * @returns A `Set` containing the popped elements, depending on the set's length. + * If `key` does not exist, an empty `Set` will be returned. * * @example + * ```typescript * // Example usage of spopCount method to remove and return multiple random members from a set * const result = await client.spopCount("my_set", 2); - * console.log(result); // Output: ['member2', 'member3'] - Removes and returns 2 random members from the set "my_set". + * console.log(result); // Output: Set {'member2', 'member3'} - Removes and returns 2 random members from the set "my_set". + * ``` * * @example * ```typescript * // Example usage of spopCount method with non-existing key * const result = await client.spopCount("non_existing_key"); - * console.log(result); // Output: [] + * console.log(result); // Output: Set {} - An empty set is returned since the key does not exist. * ``` */ - public spopCount(key: string, count: number): Promise { - return this.createWritePromise(createSPop(key, count)); + public async spopCount(key: string, count: number): Promise> { + return this.createWritePromise(createSPop(key, count)).then( + (spop) => new Set(spop), + ); } /** Returns the number of keys in `keys` that exist in the database. - * See https://redis.io/commands/exists/ for details. + * See https://valkey.io/commands/exists/ for details. * * @param keys - The keys list to check. * @returns The number of keys that exist. If the same existing key is mentioned in `keys` multiple times, @@ -1310,8 +1905,8 @@ export class BaseClient { /** Removes the specified keys. A key is ignored if it does not exist. * This command, similar to DEL, removes specified keys and ignores non-existent ones. - * However, this command does not block the server, while [DEL](https://redis.io/commands/del) does. - * See https://redis.io/commands/unlink/ for details. + * However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. + * See https://valkey.io/commands/unlink/ for details. * * @param keys - The keys we wanted to unlink. * @returns The number of keys that were unlinked. @@ -1331,7 +1926,7 @@ export class BaseClient { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `seconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/expire/ for details. + * See https://valkey.io/commands/expire/ for details. * * @param key - The key to set timeout on it. * @param seconds - The timeout in seconds. @@ -1365,7 +1960,7 @@ export class BaseClient { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/expireat/ for details. + * See https://valkey.io/commands/expireat/ for details. * * @param key - The key to set timeout on it. * @param unixSeconds - The timeout in an absolute Unix timestamp. @@ -1394,7 +1989,7 @@ export class BaseClient { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/pexpire/ for details. + * See https://valkey.io/commands/pexpire/ for details. * * @param key - The key to set timeout on it. * @param milliseconds - The timeout in milliseconds. @@ -1423,7 +2018,7 @@ export class BaseClient { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/pexpireat/ for details. + * See https://valkey.io/commands/pexpireat/ for details. * * @param key - The key to set timeout on it. * @param unixMilliseconds - The timeout in an absolute Unix timestamp. @@ -1449,7 +2044,7 @@ export class BaseClient { } /** Returns the remaining time to live of `key` that has a timeout. - * See https://redis.io/commands/ttl/ for details. + * See https://valkey.io/commands/ttl/ for details. * * @param key - The key to return its timeout. * @returns TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. @@ -1483,40 +2078,58 @@ export class BaseClient { * This method simplifies the process of invoking scripts on a Redis server by using an object that represents a Lua script. * The script loading, argument preparation, and execution will all be handled internally. If the script has not already been loaded, * it will be loaded automatically using the Redis `SCRIPT LOAD` command. After that, it will be invoked using the Redis `EVALSHA` command - * See https://redis.io/commands/script-load/ and https://redis.io/commands/evalsha/ for details. + * See https://valkey.io/commands/script-load/ and https://valkey.io/commands/evalsha/ for details. * * @param script - The Lua script to execute. * @param options - The script option that contains keys and arguments for the script. - * @returns a value that depends on the script that was executed. + * @returns A value that depends on the script that was executed. * * @example - * const luaScript = new Script("return \{ KEYS[1], ARGV[1] \}"); - * const scriptOptions = \{ - * keys: ["foo"], - * args: ["bar"], - * \}; - * await invokeScript(luaScript, scriptOptions); - * ["foo", "bar"] + * ```typescript + * const luaScript = new Script("return { KEYS[1], ARGV[1] }"); + * const scriptOptions = { + * keys: ["foo"], + * args: ["bar"], + * }; + * const result = await invokeScript(luaScript, scriptOptions); + * console.log(result); // Output: ['foo', 'bar'] + * ``` */ public invokeScript( script: Script, option?: ScriptOptions, ): Promise { - const scriptInvocation = redis_request.ScriptInvocation.create({ + const scriptInvocation = command_request.ScriptInvocation.create({ hash: script.getHash(), - keys: option?.keys, - args: option?.args, + keys: option?.keys?.map((item) => { + if (typeof item === "string") { + // Convert the string to a Uint8Array + return Buffer.from(item); + } else { + // If it's already a Uint8Array, just return it + return item; + } + }), + args: option?.args?.map((item) => { + if (typeof item === "string") { + // Convert the string to a Uint8Array + return Buffer.from(item); + } else { + // If it's already a Uint8Array, just return it + return item; + } + }), }); return this.createWritePromise(scriptInvocation); } /** Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. - * See https://redis.io/commands/zadd/ for more details. + * See https://valkey.io/commands/zadd/ for more details. * * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. - * @param options - The Zadd options. + * @param options - The ZAdd options. * @param changed - Modify the return value from the number of new elements added, to the total number of elements changed. * @returns The number of elements added to the sorted set. * If `changed` is set, returns the number of elements updated in the sorted set. @@ -1538,11 +2151,11 @@ export class BaseClient { public zadd( key: string, membersScoresMap: Record, - options?: ZaddOptions, + options?: ZAddOptions, changed?: boolean, ): Promise { return this.createWritePromise( - createZadd( + createZAdd( key, membersScoresMap, options, @@ -1554,12 +2167,12 @@ export class BaseClient { /** Increments the score of member in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). * If `key` does not exist, a new sorted set with the specified member as its sole member is created. - * See https://redis.io/commands/zadd/ for more details. + * See https://valkey.io/commands/zadd/ for more details. * * @param key - The key of the sorted set. * @param member - A member in the sorted set to increment. * @param increment - The score to increment the member. - * @param options - The Zadd options. + * @param options - The ZAdd options. * @returns The score of the member. * If there was a conflict with the options, the operation aborts and null is returned. * @@ -1581,16 +2194,16 @@ export class BaseClient { key: string, member: string, increment: number, - options?: ZaddOptions, + options?: ZAddOptions, ): Promise { return this.createWritePromise( - createZadd(key, { [member]: increment }, options, "INCR"), + createZAdd(key, { [member]: increment }, options, "INCR"), ); } /** Removes the specified members from the sorted set stored at `key`. * Specified members that are not a member of this set are ignored. - * See https://redis.io/commands/zrem/ for more details. + * See https://valkey.io/commands/zrem/ for more details. * * @param key - The key of the sorted set. * @param members - A list of members to remove from the sorted set. @@ -1612,11 +2225,11 @@ export class BaseClient { * ``` */ public zrem(key: string, members: string[]): Promise { - return this.createWritePromise(createZrem(key, members)); + return this.createWritePromise(createZRem(key, members)); } /** Returns the cardinality (number of elements) of the sorted set stored at `key`. - * See https://redis.io/commands/zcard/ for more details. + * See https://valkey.io/commands/zcard/ for more details. * * @param key - The key of the sorted set. * @returns The number of elements in the sorted set. @@ -1637,11 +2250,34 @@ export class BaseClient { * ``` */ public zcard(key: string): Promise { - return this.createWritePromise(createZcard(key)); + return this.createWritePromise(createZCard(key)); + } + + /** + * Returns the cardinality of the intersection of the sorted sets specified by `keys`. + * + * See https://valkey.io/commands/zintercard/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the sorted sets to intersect. + * @param limit - An optional argument that can be used to specify a maximum number for the + * intersection cardinality. If limit is not supplied, or if it is set to `0`, there will be no limit. + * @returns The cardinality of the intersection of the given sorted sets. + * + * since - Redis version 7.0.0. + * + * @example + * ```typescript + * const cardinality = await client.zintercard(["key1", "key2"], 10); + * console.log(cardinality); // Output: 3 - The intersection of the sorted sets at "key1" and "key2" has a cardinality of 3. + * ``` + */ + public zintercard(keys: string[], limit?: number): Promise { + return this.createWritePromise(createZInterCard(keys, limit)); } /** Returns the score of `member` in the sorted set stored at `key`. - * See https://redis.io/commands/zscore/ for more details. + * See https://valkey.io/commands/zscore/ for more details. * * @param key - The key of the sorted set. * @param member - The member whose score is to be retrieved. @@ -1671,11 +2307,11 @@ export class BaseClient { * ``` */ public zscore(key: string, member: string): Promise { - return this.createWritePromise(createZscore(key, member)); + return this.createWritePromise(createZScore(key, member)); } /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. - * See https://redis.io/commands/zcount/ for more details. + * See https://valkey.io/commands/zcount/ for more details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. @@ -1703,13 +2339,13 @@ export class BaseClient { minScore: ScoreBoundary, maxScore: ScoreBoundary, ): Promise { - return this.createWritePromise(createZcount(key, minScore, maxScore)); + return this.createWritePromise(createZCount(key, minScore, maxScore)); } /** Returns the specified range of elements in the sorted set stored at `key`. * ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. * - * See https://redis.io/commands/zrange/ for more details. + * See https://valkey.io/commands/zrange/ for more details. * To get the elements with their scores, see `zrangeWithScores`. * * @param key - The key of the sorted set. @@ -1726,8 +2362,9 @@ export class BaseClient { * // Example usage of zrange method to retrieve all members of a sorted set in ascending order * const result = await client.zrange("my_sorted_set", { start: 0, stop: -1 }); * console.log(result1); // Output: ['member1', 'member2', 'member3'] - Returns all members in ascending order. - * + * ``` * @example + * ```typescript * // Example usage of zrange method to retrieve members within a score range in ascending order * const result = await client.zrange("my_sorted_set", { * start: "negativeInfinity", @@ -1742,12 +2379,12 @@ export class BaseClient { rangeQuery: RangeByScore | RangeByLex | RangeByIndex, reverse: boolean = false, ): Promise { - return this.createWritePromise(createZrange(key, rangeQuery, reverse)); + return this.createWritePromise(createZRange(key, rangeQuery, reverse)); } /** Returns the specified range of elements with their scores in the sorted set stored at `key`. * Similar to ZRANGE but with a WITHSCORE flag. - * See https://redis.io/commands/zrange/ for more details. + * See https://valkey.io/commands/zrange/ for more details. * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. @@ -1767,8 +2404,9 @@ export class BaseClient { * type: "byScore", * }); * console.log(result); // Output: {'member1': 10.5, 'member2': 15.2} - Returns members with scores between 10 and 20 with their scores. - * + * ``` * @example + * ```typescript * // Example usage of zrangeWithScores method to retrieve members within a score range with their scores * const result = await client.zrangeWithScores("my_sorted_set", { * start: "negativeInfinity", @@ -1784,12 +2422,49 @@ export class BaseClient { reverse: boolean = false, ): Promise> { return this.createWritePromise( - createZrangeWithScores(key, rangeQuery, reverse), + createZRangeWithScores(key, rangeQuery, reverse), + ); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * To get the result directly, see `zinter_withscores`. + * + * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + * + * See https://valkey.io/commands/zinterstore/ for more details. + * + * @param destination - The key of the destination sorted set. + * @param keys - The keys of the sorted sets with possible formats: + * string[] - for keys only. + * KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * @returns The number of elements in the resulting sorted set stored at `destination`. + * + * @example + * ```typescript + * // Example usage of zinterstore command with an existing key + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + * await client.zadd("key2", {"member1": 9.5}) + * await client.zinterstore("my_sorted_set", ["key1", "key2"]) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. + * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. + * await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. + * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. + * ``` + */ + public zinterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise { + return this.createWritePromise( + createZInterstore(destination, keys, aggregationType), ); } /** Returns the length of the string value stored at `key`. - * See https://redis.io/commands/strlen/ for more details. + * See https://valkey.io/commands/strlen/ for more details. * * @param key - The key to check its length. * @returns - The length of the string value stored at key @@ -1815,7 +2490,7 @@ export class BaseClient { } /** Returns the string representation of the type of the value stored at `key`. - * See https://redis.io/commands/type/ for more details. + * See https://valkey.io/commands/type/ for more details. * * @param key - The `key` to check its data type. * @returns If the `key` exists, the type of the stored value is returned. Otherwise, a "none" string is returned. @@ -1843,7 +2518,7 @@ export class BaseClient { /** Removes and returns the members with the lowest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the lowest scores are removed and returned. * Otherwise, only one member with the lowest score is removed and returned. - * See https://redis.io/commands/zpopmin for more details. + * See https://valkey.io/commands/zpopmin for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -1869,13 +2544,13 @@ export class BaseClient { key: string, count?: number, ): Promise> { - return this.createWritePromise(createZpopmin(key, count)); + return this.createWritePromise(createZPopMin(key, count)); } /** Removes and returns the members with the highest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the highest scores are removed and returned. * Otherwise, only one member with the highest score is removed and returned. - * See https://redis.io/commands/zpopmax for more details. + * See https://valkey.io/commands/zpopmax for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -1901,11 +2576,11 @@ export class BaseClient { key: string, count?: number, ): Promise> { - return this.createWritePromise(createZpopmax(key, count)); + return this.createWritePromise(createZPopMax(key, count)); } /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. - * See https://redis.io/commands/pttl for more details. + * See https://valkey.io/commands/pttl for more details. * * @param key - The key to return its timeout. * @returns TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. @@ -1932,13 +2607,13 @@ export class BaseClient { * ``` */ public pttl(key: string): Promise { - return this.createWritePromise(createPttl(key)); + return this.createWritePromise(createPTTL(key)); } /** Removes all elements in the sorted set stored at `key` with rank between `start` and `end`. * Both `start` and `end` are zero-based indexes with 0 being the element with the lowest score. * These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. - * See https://redis.io/commands/zremrangebyrank/ for more details. + * See https://valkey.io/commands/zremrangebyrank/ for more details. * * @param key - The key of the sorted set. * @param start - The starting point of the range. @@ -1960,11 +2635,11 @@ export class BaseClient { start: number, end: number, ): Promise { - return this.createWritePromise(createZremRangeByRank(key, start, end)); + return this.createWritePromise(createZRemRangeByRank(key, start, end)); } /** Removes all elements in the sorted set stored at `key` with a score between `minScore` and `maxScore`. - * See https://redis.io/commands/zremrangebyscore/ for more details. + * See https://valkey.io/commands/zremrangebyscore/ for more details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to remove from. Can be positive/negative infinity, or specific score and inclusivity. @@ -1993,12 +2668,12 @@ export class BaseClient { maxScore: ScoreBoundary, ): Promise { return this.createWritePromise( - createZremRangeByScore(key, minScore, maxScore), + createZRemRangeByScore(key, minScore, maxScore), ); } /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. - * See https://redis.io/commands/zrank for more details. + * See https://valkey.io/commands/zrank for more details. * To get the rank of `member` with its score, see `zrankWithScore`. * * @param key - The key of the sorted set. @@ -2021,11 +2696,11 @@ export class BaseClient { * ``` */ public zrank(key: string, member: string): Promise { - return this.createWritePromise(createZrank(key, member)); + return this.createWritePromise(createZRank(key, member)); } /** Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. - * See https://redis.io/commands/zrank for more details. + * See https://valkey.io/commands/zrank for more details. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -2052,12 +2727,12 @@ export class BaseClient { key: string, member: string, ): Promise { - return this.createWritePromise(createZrank(key, member, true)); + return this.createWritePromise(createZRank(key, member, true)); } /** * Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. - * See https://redis.io/commands/xadd/ for more details. + * See https://valkey.io/commands/xadd/ for more details. * * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. @@ -2068,34 +2743,65 @@ export class BaseClient { values: [string, string][], options?: StreamAddOptions, ): Promise { - return this.createWritePromise(createXadd(key, values, options)); + return this.createWritePromise(createXAdd(key, values, options)); } /** * Trims the stream stored at `key` by evicting older entries. - * See https://redis.io/commands/xtrim/ for more details. + * See https://valkey.io/commands/xtrim/ for more details. * * @param key - the key of the stream * @param options - options detailing how to trim the stream. * @returns The number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. */ public xtrim(key: string, options: StreamTrimOptions): Promise { - return this.createWritePromise(createXtrim(key, options)); + return this.createWritePromise(createXTrim(key, options)); } /** * Reads entries from the given streams. - * See https://redis.io/commands/xread/ for more details. + * See https://valkey.io/commands/xread/ for more details. * * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. * @param options - options detailing how to read the stream. - * @returns A map between a stream key, and an array of entries in the matching key. The entries are in an [id, fields[]] format. + * @returns A map of stream keys, to a map of stream ids, to an array of entries. + * @example + * ```typescript + * const streamResults = await client.xread({"my_stream": "0-0", "writers": "0-0"}); + * console.log(result); // Output: { + * // "my_stream": { + * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], + * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // }, "writers": { + * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], + * // "1526985685298-0": [["name", "Jane"], ["surname", "Austen"]], + * // } + * // } + * ``` */ public xread( keys_and_ids: Record, options?: StreamReadOptions, - ): Promise> { - return this.createWritePromise(createXread(keys_and_ids, options)); + ): Promise>> { + return this.createWritePromise(createXRead(keys_and_ids, options)); + } + + /** + * Returns the number of entries in the stream stored at `key`. + * + * See https://valkey.io/commands/xlen/ for more details. + * + * @param key - The key of the stream. + * @returns The number of entries in the stream. If `key` does not exist, returns `0`. + * + * @example + * ```typescript + * const numEntries = await client.xlen("my_stream"); + * console.log(numEntries); // Output: 2 - "my_stream" contains 2 entries. + * ``` + */ + public xlen(key: string): Promise { + return this.createWritePromise(createXLen(key)); } private readonly MAP_READ_FROM_STRATEGY: Record< @@ -2110,7 +2816,7 @@ export class BaseClient { * The index is zero-based, so 0 means the first element, 1 the second element and so on. * Negative indices can be used to designate elements starting at the tail of the list. * Here, -1 means the last element, -2 means the penultimate and so forth. - * See https://redis.io/commands/lindex/ for more details. + * See https://valkey.io/commands/lindex/ for more details. * * @param key - The `key` of the list. * @param index - The `index` of the element in the list to retrieve. @@ -2132,12 +2838,43 @@ export class BaseClient { * ``` */ public lindex(key: string, index: number): Promise { - return this.createWritePromise(createLindex(key, index)); + return this.createWritePromise(createLIndex(key, index)); + } + + /** + * Inserts `element` in the list at `key` either before or after the `pivot`. + * + * See https://valkey.io/commands/linsert/ for more details. + * + * @param key - The key of the list. + * @param position - The relative position to insert into - either `InsertPosition.Before` or + * `InsertPosition.After` the `pivot`. + * @param pivot - An element of the list. + * @param element - The new element to insert. + * @returns The list length after a successful insert operation. + * If the `key` doesn't exist returns `-1`. + * If the `pivot` wasn't found, returns `0`. + * + * @example + * ```typescript + * const length = await client.linsert("my_list", InsertPosition.Before, "World", "There"); + * console.log(length); // Output: 2 - The list has a length of 2 after performing the insert. + * ``` + */ + public linsert( + key: string, + position: InsertPosition, + pivot: string, + element: string, + ): Promise { + return this.createWritePromise( + createLInsert(key, position, pivot, element), + ); } /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). - * See https://redis.io/commands/persist/ for more details. + * See https://valkey.io/commands/persist/ for more details. * * @param key - The key to remove the existing timeout on. * @returns `false` if `key` does not exist or does not have an associated timeout, `true` if the timeout has been removed. @@ -2156,10 +2893,9 @@ export class BaseClient { /** * Renames `key` to `newkey`. * If `newkey` already exists it is overwritten. - * In Cluster mode, both `key` and `newkey` must be in the same hash slot, - * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. - * See https://redis.io/commands/rename/ for more details. + * See https://valkey.io/commands/rename/ for more details. * + * @remarks When in cluster mode, `key` and `newKey` must map to the same hash slot. * @param key - The key to rename. * @param newKey - The new name of the key. * @returns - If the `key` was successfully renamed, return "OK". If `key` does not exist, an error is thrown. @@ -2176,18 +2912,41 @@ export class BaseClient { return this.createWritePromise(createRename(key, newKey)); } + /** + * Renames `key` to `newkey` if `newkey` does not yet exist. + * See https://valkey.io/commands/renamenx/ for more details. + * + * @remarks When in cluster mode, `key` and `newKey` must map to the same hash slot. + * @param key - The key to rename. + * @param newKey - The new name of the key. + * @returns - If the `key` was successfully renamed, returns `true`. Otherwise, returns `false`. + * If `key` does not exist, an error is thrown. + * + * @example + * ```typescript + * // Example usage of renamenx method to rename a key + * await client.set("old_key", "value"); + * const result = await client.renamenx("old_key", "new_key"); + * console.log(result); // Output: true - Indicates successful renaming of the key "old_key" to "new_key". + * ``` + */ + public renamenx(key: string, newKey: string): Promise { + return this.createWritePromise(createRenameNX(key, newKey)); + } + /** Blocking list pop primitive. * Pop an element from the tail of the first list that is non-empty, - * with the given keys being checked in the order that they are given. + * with the given `keys` being checked in the order that they are given. * Blocks the connection when there are no elements to pop from any of the given lists. - * See https://redis.io/commands/brpop/ for more details. - * Note: BRPOP is a blocking command, - * see [Blocking Commands](https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands) for more details and best practices. + * See https://valkey.io/commands/brpop/ for more details. * + * @remarks + * 1. When in cluster mode, all `keys` must map to the same hash slot. + * 2. `BRPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. * @returns - An `array` containing the `key` from which the element was popped and the value of the popped element, - * formatted as [key, value]. If no element could be popped and the timeout expired, returns Null. + * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. * * @example * ```typescript @@ -2200,7 +2959,176 @@ export class BaseClient { keys: string[], timeout: number, ): Promise<[string, string] | null> { - return this.createWritePromise(createBrpop(keys, timeout)); + return this.createWritePromise(createBRPop(keys, timeout)); + } + + /** Blocking list pop primitive. + * Pop an element from the head of the first list that is non-empty, + * with the given `keys` being checked in the order that they are given. + * Blocks the connection when there are no elements to pop from any of the given lists. + * See https://valkey.io/commands/blpop/ for more details. + * + * @remarks + * 1. When in cluster mode, all `keys` must map to the same hash slot. + * 2. `BLPOP` is a blocking command, see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * @param keys - The `keys` of the lists to pop from. + * @param timeout - The `timeout` in seconds. + * @returns - An `array` containing the `key` from which the element was popped and the value of the popped element, + * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. + * + * @example + * ```typescript + * const result = await client.blpop(["list1", "list2"], 5); + * console.log(result); // Output: ['list1', 'element'] + * ``` + */ + public blpop( + keys: string[], + timeout: number, + ): Promise<[string, string] | null> { + return this.createWritePromise(createBLPop(keys, timeout)); + } + + /** Adds all elements to the HyperLogLog data structure stored at the specified `key`. + * Creates a new structure if the `key` does not exist. + * When no elements are provided, and `key` exists and is a HyperLogLog, then no operation is performed. + * + * See https://valkey.io/commands/pfadd/ for more details. + * + * @param key - The key of the HyperLogLog data structure to add elements into. + * @param elements - An array of members to add to the HyperLogLog stored at `key`. + * @returns - If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is + * altered, then returns `1`. Otherwise, returns `0`. + * @example + * ```typescript + * const result = await client.pfadd("hll_1", ["a", "b", "c"]); + * console.log(result); // Output: 1 - Indicates that a data structure was created or modified + * const result = await client.pfadd("hll_2", []); + * console.log(result); // Output: 1 - Indicates that a new empty data structure was created + * ``` + */ + public pfadd(key: string, elements: string[]): Promise { + return this.createWritePromise(createPfAdd(key, elements)); + } + + /** Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or + * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + * + * See https://valkey.io/commands/pfcount/ for more details. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param keys - The keys of the HyperLogLog data structures to be analyzed. + * @returns - The approximated cardinality of given HyperLogLog data structures. + * The cardinality of a key that does not exist is `0`. + * @example + * ```typescript + * const result = await client.pfcount(["hll_1", "hll_2"]); + * console.log(result); // Output: 4 - The approximated cardinality of the union of "hll_1" and "hll_2" + * ``` + */ + public pfcount(keys: string[]): Promise { + return this.createWritePromise(createPfCount(keys)); + } + + /** Returns the internal encoding for the Redis object stored at `key`. + * + * See https://valkey.io/commands/object-encoding for more details. + * + * @param key - The `key` of the object to get the internal encoding of. + * @returns - If `key` exists, returns the internal encoding of the object stored at `key` as a string. + * Otherwise, returns None. + * @example + * ```typescript + * const result = await client.objectEncoding("my_hash"); + * console.log(result); // Output: "listpack" + * ``` + */ + public objectEncoding(key: string): Promise { + return this.createWritePromise(createObjectEncoding(key)); + } + + /** Returns the logarithmic access frequency counter of a Redis object stored at `key`. + * + * See https://valkey.io/commands/object-freq for more details. + * + * @param key - The `key` of the object to get the logarithmic access frequency counter of. + * @returns - If `key` exists, returns the logarithmic access frequency counter of the object + * stored at `key` as a `number`. Otherwise, returns `null`. + * @example + * ```typescript + * const result = await client.objectFreq("my_hash"); + * console.log(result); // Output: 2 - The logarithmic access frequency counter of "my_hash". + * ``` + */ + public objectFreq(key: string): Promise { + return this.createWritePromise(createObjectFreq(key)); + } + + /** + * Returns the time in seconds since the last access to the value stored at `key`. + * + * See https://valkey.io/commands/object-idletime/ for more details. + * + * @param key - The key of the object to get the idle time of. + * @returns If `key` exists, returns the idle time in seconds. Otherwise, returns `null`. + * + * @example + * ```typescript + * const result = await client.objectIdletime("my_hash"); + * console.log(result); // Output: 13 - "my_hash" was last accessed 13 seconds ago. + * ``` + */ + public objectIdletime(key: string): Promise { + return this.createWritePromise(createObjectIdletime(key)); + } + + /** + * Returns the reference count of the object stored at `key`. + * + * See https://valkey.io/commands/object-refcount/ for more details. + * + * @param key - The `key` of the object to get the reference count of. + * @returns If `key` exists, returns the reference count of the object stored at `key` as a `number`. + * Otherwise, returns `null`. + * + * @example + * ```typescript + * const result = await client.objectRefcount("my_hash"); + * console.log(result); // Output: 2 - "my_hash" has a reference count of 2. + * ``` + */ + public objectRefcount(key: string): Promise { + return this.createWritePromise(createObjectRefcount(key)); + } + + /** + * Returns the index of the first occurrence of `element` inside the list specified by `key`. If no + * match is found, `null` is returned. If the `count` option is specified, then the function returns + * an `array` of indices of matching elements within the list. + * + * See https://valkey.io/commands/lpos/ for more details. + * + * @param key - The name of the list. + * @param element - The value to search for within the list. + * @param options - The LPOS options. + * @returns The index of `element`, or `null` if `element` is not in the list. If the `count` option + * is specified, then the function returns an `array` of indices of matching elements within the list. + * + * since - Valkey version 6.0.6. + * + * @example + * ```typescript + * await client.rpush("myList", ["a", "b", "c", "d", "e", "e"]); + * console.log(await client.lpos("myList", "e", new LPosOptions({ rank: 2 }))); // Output: 5 - the second occurrence of "e" is at index 5. + * console.log(await client.lpos("myList", "e", new LPosOptions({ count: 3 }))); // Output: [ 4, 5 ] - indices for the occurrences of "e" in list "myList". + * ``` + */ + public lpos( + key: string, + element: string, + options?: LPosOptions, + ): Promise { + return this.createWritePromise(createLPos(key, element, options)); } /** @@ -2273,6 +3201,11 @@ export class BaseClient { this.promiseCallbackFunctions.forEach(([, reject]) => { reject(new ClosingError(errorMessage)); }); + + // Handle pubsub futures + this.pubsubFutures.forEach(([, reject]) => { + reject(new ClosingError(errorMessage || "")); + }); Logger.log("info", "Client lifetime", "disposing of client"); this.socket.end(); } diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 47f88c69eb..461bd4f155 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1,13 +1,16 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { MAX_REQUEST_ARGS_LEN, createLeakedStringVec } from "glide-rs"; +import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs"; import Long from "long"; -import { redis_request } from "./ProtobufMessage"; -import RequestType = redis_request.RequestType; +import { LPosOptions } from "./command-options/LPosOptions"; -function isLargeCommand(args: string[]) { +import { command_request } from "./ProtobufMessage"; + +import RequestType = command_request.RequestType; + +function isLargeCommand(args: BulkString[]) { let lenSum = 0; for (const arg of args) { @@ -21,6 +24,25 @@ function isLargeCommand(args: string[]) { return false; } +type BulkString = string | Uint8Array; + +/** + * Convert a string array into Uint8Array[] + */ +function toBuffersArray(args: BulkString[]) { + const argsBytes: Uint8Array[] = []; + + for (const arg of args) { + if (typeof arg == "string") { + argsBytes.push(Buffer.from(arg)); + } else { + argsBytes.push(arg); + } + } + + return argsBytes; +} + /** * @internal */ @@ -40,21 +62,23 @@ export function parseInfoResponse(response: string): Record { } function createCommand( - requestType: redis_request.RequestType, - args: string[], -): redis_request.Command { - const singleCommand = redis_request.Command.create({ + requestType: command_request.RequestType, + args: BulkString[], +): command_request.Command { + const singleCommand = command_request.Command.create({ requestType, }); + const argsBytes = toBuffersArray(args); + if (isLargeCommand(args)) { // pass as a pointer - const pointerArr = createLeakedStringVec(args); + const pointerArr = createLeakedStringVec(argsBytes); const pointer = new Long(pointerArr[0], pointerArr[1]); singleCommand.argsVecPointer = pointer; } else { - singleCommand.argsArray = redis_request.Command.ArgsArray.create({ - args: args, + singleCommand.argsArray = command_request.Command.ArgsArray.create({ + args: argsBytes, }); } @@ -64,20 +88,22 @@ function createCommand( /** * @internal */ -export function createGet(key: string): redis_request.Command { - return createCommand(RequestType.GetString, [key]); +export function createGet(key: string): command_request.Command { + return createCommand(RequestType.Get, [key]); } export type SetOptions = { /** - * `onlyIfDoesNotExist` - Only set the key if it does not already exist. Equivalent to `NX` in the Redis API. - * `onlyIfExists` - Only set the key if it already exist. Equivalent to `EX` in the Redis API. - * if `conditional` is not set the value will be set regardless of prior value existence. - * If value isn't set because of the condition, return null. + * `onlyIfDoesNotExist` - Only set the key if it does not already exist. + * Equivalent to `NX` in the Redis API. `onlyIfExists` - Only set the key if + * it already exist. Equivalent to `EX` in the Redis API. if `conditional` is + * not set the value will be set regardless of prior value existence. If value + * isn't set because of the condition, return null. */ conditionalSet?: "onlyIfExists" | "onlyIfDoesNotExist"; /** - * Return the old string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value stored at key is not a string. + * Return the old string stored at key, or nil if key did not exist. An error + * is returned and SET aborted if the value stored at key is not a string. * Equivalent to `GET` in the Redis API. */ returnOldValue?: boolean; @@ -85,24 +111,29 @@ export type SetOptions = { * If not set, no expiry time will be set for the value. */ expiry?: /** - * Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Redis API. + * Retain the time to live associated with the key. Equivalent to + * `KEEPTTL` in the Redis API. */ | "keepExisting" | { type: /** - * Set the specified expire time, in seconds. Equivalent to `EX` in the Redis API. + * Set the specified expire time, in seconds. Equivalent to + * `EX` in the Redis API. */ | "seconds" /** - * Set the specified expire time, in milliseconds. Equivalent to `PX` in the Redis API. + * Set the specified expire time, in milliseconds. Equivalent + * to `PX` in the Redis API. */ | "milliseconds" /** - * Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Redis API. + * Set the specified Unix time at which the key will expire, + * in seconds. Equivalent to `EXAT` in the Redis API. */ | "unixSeconds" /** - * Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the Redis API. + * Set the specified Unix time at which the key will expire, + * in milliseconds. Equivalent to `PXAT` in the Redis API. */ | "unixMilliseconds"; count: number; @@ -113,10 +144,10 @@ export type SetOptions = { * @internal */ export function createSet( - key: string, - value: string, + key: BulkString, + value: BulkString, options?: SetOptions, -): redis_request.Command { +): command_request.Command { const args = [key, value]; if (options) { @@ -145,17 +176,17 @@ export function createSet( if (options.expiry === "keepExisting") { args.push("KEEPTTL"); } else if (options.expiry?.type === "seconds") { - args.push("EX " + options.expiry.count); + args.push("EX", options.expiry.count.toString()); } else if (options.expiry?.type === "milliseconds") { - args.push("PX " + options.expiry.count); + args.push("PX", options.expiry.count.toString()); } else if (options.expiry?.type === "unixSeconds") { - args.push("EXAT " + options.expiry.count); + args.push("EXAT", options.expiry.count.toString()); } else if (options.expiry?.type === "unixMilliseconds") { - args.push("PXAT " + options.expiry.count); + args.push("PXAT", options.expiry.count.toString()); } } - return createCommand(RequestType.SetString, args); + return createCommand(RequestType.Set, args); } /** @@ -236,7 +267,7 @@ export enum InfoOptions { /** * @internal */ -export function createPing(str?: string): redis_request.Command { +export function createPing(str?: string): command_request.Command { const args: string[] = str == undefined ? [] : [str]; return createCommand(RequestType.Ping, args); } @@ -244,7 +275,7 @@ export function createPing(str?: string): redis_request.Command { /** * @internal */ -export function createInfo(options?: InfoOptions[]): redis_request.Command { +export function createInfo(options?: InfoOptions[]): command_request.Command { const args: string[] = options == undefined ? [] : options; return createCommand(RequestType.Info, args); } @@ -252,42 +283,42 @@ export function createInfo(options?: InfoOptions[]): redis_request.Command { /** * @internal */ -export function createDel(keys: string[]): redis_request.Command { +export function createDel(keys: string[]): command_request.Command { return createCommand(RequestType.Del, keys); } /** * @internal */ -export function createSelect(index: number): redis_request.Command { +export function createSelect(index: number): command_request.Command { return createCommand(RequestType.Select, [index.toString()]); } /** * @internal */ -export function createClientGetName(): redis_request.Command { +export function createClientGetName(): command_request.Command { return createCommand(RequestType.ClientGetName, []); } /** * @internal */ -export function createConfigRewrite(): redis_request.Command { +export function createConfigRewrite(): command_request.Command { return createCommand(RequestType.ConfigRewrite, []); } /** * @internal */ -export function createConfigResetStat(): redis_request.Command { +export function createConfigResetStat(): command_request.Command { return createCommand(RequestType.ConfigResetStat, []); } /** * @internal */ -export function createMGet(keys: string[]): redis_request.Command { +export function createMGet(keys: string[]): command_request.Command { return createCommand(RequestType.MGet, keys); } @@ -296,14 +327,14 @@ export function createMGet(keys: string[]): redis_request.Command { */ export function createMSet( keyValueMap: Record, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.MSet, Object.entries(keyValueMap).flat()); } /** * @internal */ -export function createIncr(key: string): redis_request.Command { +export function createIncr(key: string): command_request.Command { return createCommand(RequestType.Incr, [key]); } @@ -313,7 +344,7 @@ export function createIncr(key: string): redis_request.Command { export function createIncrBy( key: string, amount: number, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.IncrBy, [key, amount.toString()]); } @@ -323,21 +354,21 @@ export function createIncrBy( export function createIncrByFloat( key: string, amount: number, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.IncrByFloat, [key, amount.toString()]); } /** * @internal */ -export function createClientId(): redis_request.Command { +export function createClientId(): command_request.Command { return createCommand(RequestType.ClientId, []); } /** * @internal */ -export function createConfigGet(parameters: string[]): redis_request.Command { +export function createConfigGet(parameters: string[]): command_request.Command { return createCommand(RequestType.ConfigGet, parameters); } @@ -346,7 +377,7 @@ export function createConfigGet(parameters: string[]): redis_request.Command { */ export function createConfigSet( parameters: Record, -): redis_request.Command { +): command_request.Command { return createCommand( RequestType.ConfigSet, Object.entries(parameters).flat(), @@ -356,8 +387,11 @@ export function createConfigSet( /** * @internal */ -export function createHGet(key: string, field: string): redis_request.Command { - return createCommand(RequestType.HashGet, [key, field]); +export function createHGet( + key: string, + field: string, +): command_request.Command { + return createCommand(RequestType.HGet, [key, field]); } /** @@ -366,9 +400,9 @@ export function createHGet(key: string, field: string): redis_request.Command { export function createHSet( key: string, fieldValueMap: Record, -): redis_request.Command { +): command_request.Command { return createCommand( - RequestType.HashSet, + RequestType.HSet, [key].concat(Object.entries(fieldValueMap).flat()), ); } @@ -380,14 +414,14 @@ export function createHSetNX( key: string, field: string, value: string, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.HSetNX, [key, field, value]); } /** * @internal */ -export function createDecr(key: string): redis_request.Command { +export function createDecr(key: string): command_request.Command { return createCommand(RequestType.Decr, [key]); } @@ -397,7 +431,7 @@ export function createDecr(key: string): redis_request.Command { export function createDecrBy( key: string, amount: number, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.DecrBy, [key, amount.toString()]); } @@ -407,8 +441,8 @@ export function createDecrBy( export function createHDel( key: string, fields: string[], -): redis_request.Command { - return createCommand(RequestType.HashDel, [key].concat(fields)); +): command_request.Command { + return createCommand(RequestType.HDel, [key].concat(fields)); } /** @@ -417,8 +451,8 @@ export function createHDel( export function createHMGet( key: string, fields: string[], -): redis_request.Command { - return createCommand(RequestType.HashMGet, [key].concat(fields)); +): command_request.Command { + return createCommand(RequestType.HMGet, [key].concat(fields)); } /** @@ -427,15 +461,15 @@ export function createHMGet( export function createHExists( key: string, field: string, -): redis_request.Command { - return createCommand(RequestType.HashExists, [key, field]); +): command_request.Command { + return createCommand(RequestType.HExists, [key, field]); } /** * @internal */ -export function createHGetAll(key: string): redis_request.Command { - return createCommand(RequestType.HashGetAll, [key]); +export function createHGetAll(key: string): command_request.Command { + return createCommand(RequestType.HGetAll, [key]); } /** @@ -444,14 +478,27 @@ export function createHGetAll(key: string): redis_request.Command { export function createLPush( key: string, elements: string[], -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.LPush, [key].concat(elements)); } /** * @internal */ -export function createLPop(key: string, count?: number): redis_request.Command { +export function createLPushX( + key: string, + elements: string[], +): command_request.Command { + return createCommand(RequestType.LPushX, [key].concat(elements)); +} + +/** + * @internal + */ +export function createLPop( + key: string, + count?: number, +): command_request.Command { const args: string[] = count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.LPop, args); } @@ -463,7 +510,7 @@ export function createLRange( key: string, start: number, end: number, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.LRange, [ key, start.toString(), @@ -474,10 +521,21 @@ export function createLRange( /** * @internal */ -export function createLLen(key: string): redis_request.Command { +export function createLLen(key: string): command_request.Command { return createCommand(RequestType.LLen, [key]); } +/** + * @internal + */ +export function createLSet( + key: string, + index: number, + element: string, +): command_request.Command { + return createCommand(RequestType.LSet, [key, index.toString(), element]); +} + /** * @internal */ @@ -485,7 +543,7 @@ export function createLTrim( key: string, start: number, end: number, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.LTrim, [ key, start.toString(), @@ -500,7 +558,7 @@ export function createLRem( key: string, count: number, element: string, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.LRem, [key, count.toString(), element]); } @@ -510,14 +568,27 @@ export function createLRem( export function createRPush( key: string, elements: string[], -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.RPush, [key].concat(elements)); } /** * @internal */ -export function createRPop(key: string, count?: number): redis_request.Command { +export function createRPushX( + key: string, + elements: string[], +): command_request.Command { + return createCommand(RequestType.RPushX, [key].concat(elements)); +} + +/** + * @internal + */ +export function createRPop( + key: string, + count?: number, +): command_request.Command { const args: string[] = count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.RPop, args); } @@ -528,7 +599,7 @@ export function createRPop(key: string, count?: number): redis_request.Command { export function createSAdd( key: string, members: string[], -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.SAdd, [key].concat(members)); } @@ -538,40 +609,133 @@ export function createSAdd( export function createSRem( key: string, members: string[], -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.SRem, [key].concat(members)); } /** * @internal */ -export function createSMembers(key: string): redis_request.Command { +export function createSMembers(key: string): command_request.Command { return createCommand(RequestType.SMembers, [key]); } +/** + * + * @internal + */ +export function createSMove( + source: string, + destination: string, + member: string, +): command_request.Command { + return createCommand(RequestType.SMove, [source, destination, member]); +} + /** * @internal */ -export function createSCard(key: string): redis_request.Command { +export function createSCard(key: string): command_request.Command { return createCommand(RequestType.SCard, [key]); } /** * @internal */ -export function createSismember( +export function createSInter(keys: string[]): command_request.Command { + return createCommand(RequestType.SInter, keys); +} + +/** + * @internal + */ +export function createSInterCard( + keys: string[], + limit?: number, +): command_request.Command { + let args: string[] = keys; + args.unshift(keys.length.toString()); + + if (limit != undefined) { + args = args.concat(["LIMIT", limit.toString()]); + } + + return createCommand(RequestType.SInterCard, args); +} + +/** + * @internal + */ +export function createSInterStore( + destination: string, + keys: string[], +): command_request.Command { + return createCommand(RequestType.SInterStore, [destination].concat(keys)); +} + +/** + * @internal + */ +export function createSDiff(keys: string[]): command_request.Command { + return createCommand(RequestType.SDiff, keys); +} + +/** + * @internal + */ +export function createSDiffStore( + destination: string, + keys: string[], +): command_request.Command { + return createCommand(RequestType.SDiffStore, [destination].concat(keys)); +} + +/** + * @internal + */ +export function createSUnion(keys: string[]): command_request.Command { + return createCommand(RequestType.SUnion, keys); +} + +/** + * @internal + */ +export function createSUnionStore( + destination: string, + keys: string[], +): command_request.Command { + return createCommand(RequestType.SUnionStore, [destination].concat(keys)); +} + +/** + * @internal + */ +export function createSIsMember( key: string, member: string, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.SIsMember, [key, member]); } /** * @internal */ -export function createSPop(key: string, count?: number): redis_request.Command { +export function createSMIsMember( + key: string, + members: string[], +): command_request.Command { + return createCommand(RequestType.SMIsMember, [key].concat(members)); +} + +/** + * @internal + */ +export function createSPop( + key: string, + count?: number, +): command_request.Command { const args: string[] = count == undefined ? [key] : [key, count.toString()]; - return createCommand(RequestType.Spop, args); + return createCommand(RequestType.SPop, args); } /** @@ -588,12 +752,8 @@ export function createHIncrBy( key: string, field: string, amount: number, -): redis_request.Command { - return createCommand(RequestType.HashIncrBy, [ - key, - field, - amount.toString(), - ]); +): command_request.Command { + return createCommand(RequestType.HIncrBy, [key, field, amount.toString()]); } /** @@ -603,8 +763,8 @@ export function createHIncrByFloat( key: string, field: string, amount: number, -): redis_request.Command { - return createCommand(RequestType.HashIncrByFloat, [ +): command_request.Command { + return createCommand(RequestType.HIncrByFloat, [ key, field, amount.toString(), @@ -614,28 +774,28 @@ export function createHIncrByFloat( /** * @internal */ -export function createHLen(key: string): redis_request.Command { +export function createHLen(key: string): command_request.Command { return createCommand(RequestType.HLen, [key]); } /** * @internal */ -export function createHvals(key: string): redis_request.Command { - return createCommand(RequestType.Hvals, [key]); +export function createHVals(key: string): command_request.Command { + return createCommand(RequestType.HVals, [key]); } /** * @internal */ -export function createExists(keys: string[]): redis_request.Command { +export function createExists(keys: string[]): command_request.Command { return createCommand(RequestType.Exists, keys); } /** * @internal */ -export function createUnlink(keys: string[]): redis_request.Command { +export function createUnlink(keys: string[]): command_request.Command { return createCommand(RequestType.Unlink, keys); } @@ -649,11 +809,13 @@ export enum ExpireOptions { */ HasExistingExpiry = "XX", /** - * `NewExpiryGreaterThanCurrent` - Sets expiry only when the new expiry is greater than current one. + * `NewExpiryGreaterThanCurrent` - Sets expiry only when the new expiry is + * greater than current one. */ NewExpiryGreaterThanCurrent = "GT", /** - * `NewExpiryLessThanCurrent` - Sets expiry only when the new expiry is less than current one. + * `NewExpiryLessThanCurrent` - Sets expiry only when the new expiry is less + * than current one. */ NewExpiryLessThanCurrent = "LT", } @@ -665,7 +827,7 @@ export function createExpire( key: string, seconds: number, option?: ExpireOptions, -): redis_request.Command { +): command_request.Command { const args: string[] = option == undefined ? [key, seconds.toString()] @@ -680,7 +842,7 @@ export function createExpireAt( key: string, unixSeconds: number, option?: ExpireOptions, -): redis_request.Command { +): command_request.Command { const args: string[] = option == undefined ? [key, unixSeconds.toString()] @@ -695,7 +857,7 @@ export function createPExpire( key: string, milliseconds: number, option?: ExpireOptions, -): redis_request.Command { +): command_request.Command { const args: string[] = option == undefined ? [key, milliseconds.toString()] @@ -710,7 +872,7 @@ export function createPExpireAt( key: string, unixMilliseconds: number, option?: ExpireOptions, -): redis_request.Command { +): command_request.Command { const args: string[] = option == undefined ? [key, unixMilliseconds.toString()] @@ -721,21 +883,23 @@ export function createPExpireAt( /** * @internal */ -export function createTTL(key: string): redis_request.Command { +export function createTTL(key: string): command_request.Command { return createCommand(RequestType.TTL, [key]); } -export type ZaddOptions = { +export type ZAddOptions = { /** - * `onlyIfDoesNotExist` - Only add new elements. Don't update already existing elements. Equivalent to `NX` in the Redis API. - * `onlyIfExists` - Only update elements that already exist. Don't add new elements. Equivalent to `XX` in the Redis API. + * `onlyIfDoesNotExist` - Only add new elements. Don't update already existing + * elements. Equivalent to `NX` in the Redis API. `onlyIfExists` - Only update + * elements that already exist. Don't add new elements. Equivalent to `XX` in + * the Redis API. */ conditionalChange?: "onlyIfExists" | "onlyIfDoesNotExist"; /** - * `scoreLessThanCurrent` - Only update existing elements if the new score is less than the current score. - * Equivalent to `LT` in the Redis API. - * `scoreGreaterThanCurrent` - Only update existing elements if the new score is greater than the current score. - * Equivalent to `GT` in the Redis API. + * `scoreLessThanCurrent` - Only update existing elements if the new score is + * less than the current score. Equivalent to `LT` in the Redis API. + * `scoreGreaterThanCurrent` - Only update existing elements if the new score + * is greater than the current score. Equivalent to `GT` in the Redis API. */ updateOptions?: "scoreLessThanCurrent" | "scoreGreaterThanCurrent"; }; @@ -743,12 +907,12 @@ export type ZaddOptions = { /** * @internal */ -export function createZadd( +export function createZAdd( key: string, membersScoresMap: Record, - options?: ZaddOptions, + options?: ZAddOptions, changedOrIncr?: "CH" | "INCR", -): redis_request.Command { +): command_request.Command { let args = [key]; if (options) { @@ -781,33 +945,94 @@ export function createZadd( key, ]), ); - return createCommand(RequestType.Zadd, args); + return createCommand(RequestType.ZAdd, args); } +/** + * `KeyWeight` - pair of variables represents a weighted key for the `ZINTERSTORE` and `ZUNIONSTORE` sorted sets commands. + */ +export type KeyWeight = [string, number]; +/** + * `AggregationType` - representing aggregation types for `ZINTERSTORE` and `ZUNIONSTORE` sorted set commands. + */ +export type AggregationType = "SUM" | "MIN" | "MAX"; + /** * @internal */ -export function createZrem( +export function createZInterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): command_request.Command { + const args = createZCmdStoreArgs(destination, keys, aggregationType); + return createCommand(RequestType.ZInterStore, args); +} + +function createZCmdStoreArgs( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): string[] { + const args: string[] = [destination, keys.length.toString()]; + + if (typeof keys[0] === "string") { + args.push(...(keys as string[])); + } else { + const weightsKeys = keys.map(([key]) => key); + args.push(...(weightsKeys as string[])); + const weights = keys.map(([, weight]) => weight.toString()); + args.push("WEIGHTS", ...weights); + } + + if (aggregationType) { + args.push("AGGREGATE", aggregationType); + } + + return args; +} + +/** + * @internal + */ +export function createZRem( key: string, members: string[], -): redis_request.Command { - return createCommand(RequestType.Zrem, [key].concat(members)); +): command_request.Command { + return createCommand(RequestType.ZRem, [key].concat(members)); +} + +/** + * @internal + */ +export function createZCard(key: string): command_request.Command { + return createCommand(RequestType.ZCard, [key]); } /** * @internal */ -export function createZcard(key: string): redis_request.Command { - return createCommand(RequestType.Zcard, [key]); +export function createZInterCard( + keys: string[], + limit?: number, +): command_request.Command { + let args: string[] = keys; + args.unshift(keys.length.toString()); + + if (limit != undefined) { + args = args.concat(["LIMIT", limit.toString()]); + } + + return createCommand(RequestType.ZInterCard, args); } /** * @internal */ -export function createZscore( +export function createZScore( key: string, member: string, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.ZScore, [key, member]); } @@ -865,10 +1090,10 @@ type SortedSetRange = { /** * The limit argument for a range query. * Represents a limit argument for a range query in a sorted set to - * be used in [ZRANGE](https://redis.io/commands/zrange) command. + * be used in [ZRANGE](https://valkey.io/commands/zrange) command. * - * The optional LIMIT argument can be used to obtain a sub-range from the matching elements - * (similar to SELECT LIMIT offset, count in SQL). + * The optional LIMIT argument can be used to obtain a sub-range from the + * matching elements (similar to SELECT LIMIT offset, count in SQL). */ limit?: { /** @@ -888,9 +1113,12 @@ export type RangeByLex = SortedSetRange & { type: "byLex" }; /** * Returns a string representation of a score boundary in Redis protocol format. - * @param score - The score boundary object containing value and inclusivity information. - * @param isLex - Indicates whether to return lexical representation for positive/negative infinity. - * @returns A string representation of the score boundary in Redis protocol format. + * @param score - The score boundary object containing value and inclusivity + * information. + * @param isLex - Indicates whether to return lexical representation for + * positive/negative infinity. + * @returns A string representation of the score boundary in Redis protocol + * format. */ function getScoreBoundaryArg( score: ScoreBoundary | ScoreBoundary, @@ -910,7 +1138,7 @@ function getScoreBoundaryArg( return value; } -function createZrangeArgs( +function createZRangeArgs( key: string, rangeQuery: RangeByScore | RangeByLex | RangeByIndex, reverse: boolean, @@ -951,72 +1179,98 @@ function createZrangeArgs( /** * @internal */ -export function createZcount( +export function createZCount( key: string, minScore: ScoreBoundary, maxScore: ScoreBoundary, -): redis_request.Command { +): command_request.Command { const args = [key]; args.push(getScoreBoundaryArg(minScore)); args.push(getScoreBoundaryArg(maxScore)); - return createCommand(RequestType.Zcount, args); + return createCommand(RequestType.ZCount, args); } /** * @internal */ -export function createZrange( +export function createZRange( key: string, rangeQuery: RangeByIndex | RangeByScore | RangeByLex, reverse: boolean = false, -): redis_request.Command { - const args = createZrangeArgs(key, rangeQuery, reverse, false); - return createCommand(RequestType.Zrange, args); +): command_request.Command { + const args = createZRangeArgs(key, rangeQuery, reverse, false); + return createCommand(RequestType.ZRange, args); } /** * @internal */ -export function createZrangeWithScores( +export function createZRangeWithScores( key: string, rangeQuery: RangeByIndex | RangeByScore | RangeByLex, reverse: boolean = false, -): redis_request.Command { - const args = createZrangeArgs(key, rangeQuery, reverse, true); - return createCommand(RequestType.Zrange, args); +): command_request.Command { + const args = createZRangeArgs(key, rangeQuery, reverse, true); + return createCommand(RequestType.ZRange, args); } /** * @internal */ -export function createType(key: string): redis_request.Command { +export function createType(key: string): command_request.Command { return createCommand(RequestType.Type, [key]); } /** * @internal */ -export function createStrlen(key: string): redis_request.Command { +export function createStrlen(key: string): command_request.Command { return createCommand(RequestType.Strlen, [key]); } /** * @internal */ -export function createLindex( +export function createLIndex( key: string, index: number, -): redis_request.Command { - return createCommand(RequestType.Lindex, [key, index.toString()]); +): command_request.Command { + return createCommand(RequestType.LIndex, [key, index.toString()]); +} + +/** + * Defines where to insert new elements into a list. + */ +export enum InsertPosition { + /** + * Insert new element before the pivot. + */ + Before = "before", + /** + * Insert new element after the pivot. + */ + After = "after", +} + +/** + * @internal + */ +export function createLInsert( + key: string, + position: InsertPosition, + pivot: string, + element: string, +): command_request.Command { + return createCommand(RequestType.LInsert, [key, position, pivot, element]); } /** * @internal */ -export function createZpopmin( +export function createZPopMin( key: string, count?: number, -): redis_request.Command { +): command_request.Command { const args: string[] = count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.ZPopMin, args); } @@ -1024,10 +1278,10 @@ export function createZpopmin( /** * @internal */ -export function createZpopmax( +export function createZPopMax( key: string, count?: number, -): redis_request.Command { +): command_request.Command { const args: string[] = count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.ZPopMax, args); } @@ -1035,25 +1289,25 @@ export function createZpopmax( /** * @internal */ -export function createEcho(message: string): redis_request.Command { +export function createEcho(message: string): command_request.Command { return createCommand(RequestType.Echo, [message]); } /** * @internal */ -export function createPttl(key: string): redis_request.Command { +export function createPTTL(key: string): command_request.Command { return createCommand(RequestType.PTTL, [key]); } /** * @internal */ -export function createZremRangeByRank( +export function createZRemRangeByRank( key: string, start: number, stop: number, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.ZRemRangeByRank, [ key, start.toString(), @@ -1064,33 +1318,33 @@ export function createZremRangeByRank( /** * @internal */ -export function createZremRangeByScore( +export function createZRemRangeByScore( key: string, minScore: ScoreBoundary, maxScore: ScoreBoundary, -): redis_request.Command { +): command_request.Command { const args = [key]; args.push(getScoreBoundaryArg(minScore)); args.push(getScoreBoundaryArg(maxScore)); return createCommand(RequestType.ZRemRangeByScore, args); } -export function createPersist(key: string): redis_request.Command { +export function createPersist(key: string): command_request.Command { return createCommand(RequestType.Persist, [key]); } -export function createZrank( +export function createZRank( key: string, member: string, withScores?: boolean, -): redis_request.Command { +): command_request.Command { const args = [key, member]; if (withScores) { args.push("WITHSCORE"); } - return createCommand(RequestType.Zrank, args); + return createCommand(RequestType.ZRank, args); } export type StreamTrimOptions = ( @@ -1112,7 +1366,9 @@ export type StreamTrimOptions = ( } ) & { /** - * If `true`, the stream will be trimmed exactly. Equivalent to `=` in the Redis API. Otherwise the stream will be trimmed in a near-exact manner, which is more efficient, equivalent to `~` in the Redis API. + * If `true`, the stream will be trimmed exactly. Equivalent to `=` in the + * Redis API. Otherwise the stream will be trimmed in a near-exact manner, + * which is more efficient, equivalent to `~` in the Redis API. */ exact: boolean; /** @@ -1127,8 +1383,8 @@ export type StreamAddOptions = { */ id?: string; /** - * If set to `false`, a new stream won't be created if no stream matches the given key. - * Equivalent to `NOMKSTREAM` in the Redis API. + * If set to `false`, a new stream won't be created if no stream matches the + * given key. Equivalent to `NOMKSTREAM` in the Redis API. */ makeStream?: boolean; /** @@ -1162,11 +1418,11 @@ function addTrimOptions(options: StreamTrimOptions, args: string[]) { } } -export function createXadd( +export function createXAdd( key: string, values: [string, string][], options?: StreamAddOptions, -): redis_request.Command { +): command_request.Command { const args = [key]; if (options?.makeStream === false) { @@ -1194,10 +1450,10 @@ export function createXadd( /** * @internal */ -export function createXtrim( +export function createXTrim( key: string, options: StreamTrimOptions, -): redis_request.Command { +): command_request.Command { const args = [key]; addTrimOptions(options, args); return createCommand(RequestType.XTrim, args); @@ -1206,25 +1462,49 @@ export function createXtrim( /** * @internal */ -export function createTime(): redis_request.Command { +export function createTime(): command_request.Command { return createCommand(RequestType.Time, []); } /** * @internal */ -export function createBrpop( +export function createPublish( + message: string, + channel: string, + sharded: boolean = false, +): command_request.Command { + const request = sharded ? RequestType.SPublish : RequestType.Publish; + return createCommand(request, [channel, message]); +} + +/** + * @internal + */ +export function createBRPop( + keys: string[], + timeout: number, +): command_request.Command { + const args = [...keys, timeout.toString()]; + return createCommand(RequestType.BRPop, args); +} + +/** + * @internal + */ +export function createBLPop( keys: string[], timeout: number, -): redis_request.Command { +): command_request.Command { const args = [...keys, timeout.toString()]; - return createCommand(RequestType.Brpop, args); + return createCommand(RequestType.BLPop, args); } export type StreamReadOptions = { /** - * If set, the read request will block for the set amount of milliseconds or until the server has the required number of entries. - * Equivalent to `BLOCK` in the Redis API. + * If set, the read request will block for the set amount of milliseconds or + * until the server has the required number of entries. Equivalent to `BLOCK` + * in the Redis API. */ block?: number; /** @@ -1263,10 +1543,10 @@ function addStreamsArgs(keys_and_ids: Record, args: string[]) { /** * @internal */ -export function createXread( +export function createXRead( keys_and_ids: Record, options?: StreamReadOptions, -): redis_request.Command { +): command_request.Command { const args: string[] = []; if (options) { @@ -1278,12 +1558,160 @@ export function createXread( return createCommand(RequestType.XRead, args); } +/** + * @internal + */ +export function createXLen(key: string): command_request.Command { + return createCommand(RequestType.XLen, [key]); +} + /** * @internal */ export function createRename( key: string, newKey: string, -): redis_request.Command { +): command_request.Command { return createCommand(RequestType.Rename, [key, newKey]); } + +/** + * @internal + */ +export function createRenameNX( + key: string, + newKey: string, +): command_request.Command { + return createCommand(RequestType.RenameNX, [key, newKey]); +} + +/** + * @internal + */ +export function createPfAdd( + key: string, + elements: string[], +): command_request.Command { + const args = [key, ...elements]; + return createCommand(RequestType.PfAdd, args); +} + +/** + * @internal + */ +export function createPfCount(keys: string[]): command_request.Command { + return createCommand(RequestType.PfCount, keys); +} + +/** + * @internal + */ +export function createObjectEncoding(key: string): command_request.Command { + return createCommand(RequestType.ObjectEncoding, [key]); +} + +/** + * @internal + */ +export function createObjectFreq(key: string): command_request.Command { + return createCommand(RequestType.ObjectFreq, [key]); +} + +/** + * @internal + */ +export function createObjectIdletime(key: string): command_request.Command { + return createCommand(RequestType.ObjectIdleTime, [key]); +} + +/** + * @internal + */ +export function createObjectRefcount(key: string): command_request.Command { + return createCommand(RequestType.ObjectRefCount, [key]); +} + +export type LolwutOptions = { + /** + * An optional argument that can be used to specify the version of computer art to generate. + */ + version?: number; + /** + * An optional argument that can be used to specify the output: + * For version `5`, those are length of the line, number of squares per row, and number of squares per column. + * For version `6`, those are number of columns and number of lines. + */ + parameters?: number[]; +}; + +/** + * @internal + */ +export function createLolwut(options?: LolwutOptions): command_request.Command { + const args: string[] = []; + + if (options) { + if (options.version !== undefined) { + args.push("VERSION", options.version.toString()); + } + + if (options.parameters !== undefined) { + args.push(...options.parameters.map((param) => param.toString())); + } + } + + return createCommand(RequestType.Lolwut, args); +} + +/** + * Defines flushing mode for: + * + * `FLUSHALL` command. + * + * See https://valkey.io/commands/flushall/ for details. + */ +export enum FlushMode { + /** + * Flushes synchronously. + * + * since Valkey 6.2 and above. + */ + SYNC = "SYNC", + /** Flushes asynchronously. */ + ASYNC = "ASYNC", +} + +/** + * @internal + */ +export function createFlushAll(mode?: FlushMode): command_request.Command { + if (mode) { + return createCommand(RequestType.FlushAll, [mode.toString()]); + } else { + return createCommand(RequestType.FlushAll, []); + } +} + +/** + * @internal + */ +export function createLPos( + key: string, + element: string, + options?: LPosOptions, +): command_request.Command { + let args: string[] = [key, element]; + + if (options) { + args = args.concat(options.toArgs()); + } + + return createCommand(RequestType.LPos, args); +} + +/** + * @internal + */ +export function createDBSize(): command_request.Command { + return createCommand(RequestType.DBSize, []); +} diff --git a/node/src/Errors.ts b/node/src/Errors.ts index 262bd03617..8fa95fa6dd 100644 --- a/node/src/Errors.ts +++ b/node/src/Errors.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ /// Base class for errors. @@ -32,3 +32,6 @@ export class ExecAbortError extends RequestError {} /// Errors that are thrown when a connection disconnects. These errors can be temporary, as the client will attempt to reconnect. export class ConnectionError extends RequestError {} + +/// Errors that are thrown when a request cannot be completed in current configuration settings. +export class ConfigurationError extends RequestError {} diff --git a/node/src/RedisClient.ts b/node/src/GlideClient.ts similarity index 60% rename from node/src/RedisClient.ts rename to node/src/GlideClient.ts index cfa6db50b5..a6700cd941 100644 --- a/node/src/RedisClient.ts +++ b/node/src/GlideClient.ts @@ -1,11 +1,18 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import * as net from "net"; -import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; import { + BaseClient, + BaseClientConfiguration, + PubSubMsg, + ReturnType, +} from "./BaseClient"; +import { + FlushMode, InfoOptions, + LolwutOptions, createClientGetName, createClientId, createConfigGet, @@ -13,16 +20,58 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createEcho, + createFlushAll, createInfo, + createLolwut, createPing, + createPublish, createSelect, createTime, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; import { Transaction } from "./Transaction"; -export type RedisClientConfiguration = BaseClientConfiguration & { +/* eslint-disable-next-line @typescript-eslint/no-namespace */ +export namespace GlideClientConfiguration { + /** + * Enum representing pubsub subscription modes. + * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. + */ + export enum PubSubChannelModes { + /** + * Use exact channel names. + */ + Exact = 0, + + /** + * Use channel name patterns. + */ + Pattern = 1, + } + + export type PubSubSubscriptions = { + /** + * Channels and patterns by modes. + */ + channelsAndPatterns: Partial>>; + + /** + * Optional callback to accept the incoming messages. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + callback?: (msg: PubSubMsg, context: any) => void; + + /** + * Arbitrary context to pass to the callback. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + context?: any; + }; +} + +export type GlideClientConfiguration = BaseClientConfiguration & { /** * index of the logical database to connect to. */ @@ -52,43 +101,50 @@ export type RedisClientConfiguration = BaseClientConfiguration & { */ exponentBase: number; }; + /** + * PubSub subscriptions to be used for the client. + * Will be applied via SUBSCRIBE/PSUBSCRIBE commands during connection establishment. + */ + pubsubSubscriptions?: GlideClientConfiguration.PubSubSubscriptions; }; /** * Client used for connection to standalone Redis servers. * For full documentation, see - * https://github.com/aws/babushka/wiki/NodeJS-wrapper#redis-standalone + * https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#standalone */ -export class RedisClient extends BaseClient { +export class GlideClient extends BaseClient { /** * @internal */ protected createClientRequest( - options: RedisClientConfiguration, + options: GlideClientConfiguration, ): connection_request.IConnectionRequest { const configuration = super.createClientRequest(options); configuration.databaseId = options.databaseId; configuration.connectionRetryStrategy = options.connectionBackoff; + this.configurePubsub(options, configuration); return configuration; } public static createClient( - options: RedisClientConfiguration, - ): Promise { - return super.createClientInternal( + options: GlideClientConfiguration, + ): Promise { + return super.createClientInternal( options, - (socket: net.Socket) => new RedisClient(socket), + (socket: net.Socket, options?: GlideClientConfiguration) => + new GlideClient(socket, options), ); } static async __createClient( options: BaseClientConfiguration, connectedSocket: net.Socket, - ): Promise { + ): Promise { return this.__createClientInternal( options, connectedSocket, - (socket, options) => new RedisClient(socket, options), + (socket, options) => new GlideClient(socket, options), ); } @@ -102,13 +158,21 @@ export class RedisClient extends BaseClient { * If the transaction failed due to a WATCH command, `exec` will return `null`. */ public exec(transaction: Transaction): Promise { - return this.createWritePromise(transaction.commands); + return this.createWritePromise( + transaction.commands, + ).then((result: ReturnType[] | null) => { + return this.processResultWithSetCommands( + result, + transaction.setCommandsIndexes, + ); + }); } /** Executes a single command, without checking inputs. Every part of the command, including subcommands, * should be added as a separate value in args. * - * @remarks - This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + * for details on the restrictions and limitations of the custom command API. * * @example * ```typescript @@ -122,7 +186,7 @@ export class RedisClient extends BaseClient { } /** Ping the Redis server. - * See https://redis.io/commands/ping/ for details. + * See https://valkey.io/commands/ping/ for details. * * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". @@ -148,7 +212,7 @@ export class RedisClient extends BaseClient { } /** Get information and statistics about the Redis server. - * See https://redis.io/commands/info/ for details. + * See https://valkey.io/commands/info/ for details. * * @param options - A list of InfoSection values specifying which sections of information to retrieve. * When no parameter is provided, the default option is assumed. @@ -159,7 +223,7 @@ export class RedisClient extends BaseClient { } /** Change the currently selected Redis database. - * See https://redis.io/commands/select/ for details. + * See https://valkey.io/commands/select/ for details. * * @param index - The index of the database to select. * @returns A simple OK response. @@ -176,7 +240,7 @@ export class RedisClient extends BaseClient { } /** Get the name of the primary's connection. - * See https://redis.io/commands/client-getname/ for more details. + * See https://valkey.io/commands/client-getname/ for more details. * * @returns the name of the client connection as a string if a name is set, or null if no name is assigned. * @@ -192,7 +256,7 @@ export class RedisClient extends BaseClient { } /** Rewrite the configuration file with the current configuration. - * See https://redis.io/commands/config-rewrite/ for details. + * See https://valkey.io/commands/config-rewrite/ for details. * * @returns "OK" when the configuration was rewritten properly. Otherwise, an error is thrown. * @@ -208,7 +272,7 @@ export class RedisClient extends BaseClient { } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - * See https://redis.io/commands/config-resetstat/ for details. + * See https://valkey.io/commands/config-resetstat/ for details. * * @returns always "OK". * @@ -224,7 +288,7 @@ export class RedisClient extends BaseClient { } /** Returns the current connection id. - * See https://redis.io/commands/client-id/ for details. + * See https://valkey.io/commands/client-id/ for details. * * @returns the id of the client. */ @@ -233,7 +297,7 @@ export class RedisClient extends BaseClient { } /** Reads the configuration parameters of a running Redis server. - * See https://redis.io/commands/config-get/ for details. + * See https://valkey.io/commands/config-get/ for details. * * @param parameters - A list of configuration parameter names to retrieve values for. * @@ -251,7 +315,7 @@ export class RedisClient extends BaseClient { } /** Set configuration parameters to the specified values. - * See https://redis.io/commands/config-set/ for details. + * See https://valkey.io/commands/config-set/ for details. * * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. * @@ -269,7 +333,7 @@ export class RedisClient extends BaseClient { } /** Echoes the provided `message` back. - * See https://redis.io/commands/echo for more details. + * See https://valkey.io/commands/echo for more details. * * @param message - The message to be echoed back. * @returns The provided `message`. @@ -277,8 +341,8 @@ export class RedisClient extends BaseClient { * @example * ```typescript * // Example usage of the echo command - * const echoedMessage = await client.echo("Glide-for-Redis"); - * console.log(echoedMessage); // Output: 'Glide-for-Redis' + * const echoedMessage = await client.echo("valkey-glide"); + * console.log(echoedMessage); // Output: 'valkey-glide' * ``` */ public echo(message: string): Promise { @@ -286,7 +350,7 @@ export class RedisClient extends BaseClient { } /** Returns the server time - * See https://redis.io/commands/time/ for details. + * See https://valkey.io/commands/time/ for details. * * @returns - The current server time as a two items `array`: * A Unix timestamp and the amount of microseconds already elapsed in the current second. @@ -302,4 +366,81 @@ export class RedisClient extends BaseClient { public time(): Promise<[string, string]> { return this.createWritePromise(createTime()); } + + /** + * Displays a piece of generative computer art and the server version. + * + * See https://valkey.io/commands/lolwut/ for more details. + * + * @param options - The LOLWUT options + * @returns A piece of generative computer art along with the current server version. + * + * @example + * ```typescript + * const response = await client.lolwut({ version: 6, parameters: [40, 20] }); + * console.log(response); // Output: "Redis ver. 7.2.3" - Indicates the current server version. + * ``` + */ + public lolwut(options?: LolwutOptions): Promise { + return this.createWritePromise(createLolwut(options)); + } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * The command will be routed to all primary nodes. + * + * See https://valkey.io/commands/flushall/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @returns `OK`. + * + * @example + * ```typescript + * const result = await client.flushall(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public flushall(mode?: FlushMode): Promise { + if (mode) { + return this.createWritePromise(createFlushAll(mode)); + } else { + return this.createWritePromise(createFlushAll()); + } + } + + /** + * Returns the number of keys in the currently selected database. + * + * See https://valkey.io/commands/dbsize/ for more details. + * + * @returns The number of keys in the currently selected database. + * + * @example + * ```typescript + * const numKeys = await client.dbsize(); + * console.log("Number of keys in the current database: ", numKeys); + * ``` + */ + public dbsize(): Promise { + return this.createWritePromise(createDBSize()); + } + + /** Publish a message on pubsub channel. + * See https://valkey.io/commands/publish for more details. + * + * @param message - Message to publish. + * @param channel - Channel to publish the message on. + * @returns - Number of subscriptions in primary node that received the message. + * Note that this value does not include subscriptions that configured on replicas. + * + * @example + * ```typescript + * // Example usage of publish command + * const result = await client.publish("Hi all!", "global-channel"); + * console.log(result); // Output: 1 - This message was posted to 1 subscription which is configured on primary node + * ``` + */ + public publish(message: string, channel: string): Promise { + return this.createWritePromise(createPublish(message, channel)); + } } diff --git a/node/src/RedisClusterClient.ts b/node/src/GlideClusterClient.ts similarity index 68% rename from node/src/RedisClusterClient.ts rename to node/src/GlideClusterClient.ts index 517e3f4d74..88a2baec1d 100644 --- a/node/src/RedisClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -1,11 +1,18 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import * as net from "net"; -import { BaseClient, BaseClientConfiguration, ReturnType } from "./BaseClient"; import { + BaseClient, + BaseClientConfiguration, + PubSubMsg, + ReturnType, +} from "./BaseClient"; +import { + FlushMode, InfoOptions, + LolwutOptions, createClientGetName, createClientId, createConfigGet, @@ -13,13 +20,17 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createEcho, + createFlushAll, createInfo, + createLolwut, createPing, + createPublish, createTime, } from "./Commands"; import { RequestError } from "./Errors"; -import { connection_request, redis_request } from "./ProtobufMessage"; +import { command_request, connection_request } from "./ProtobufMessage"; import { ClusterTransaction } from "./Transaction"; /** @@ -48,6 +59,49 @@ export type PeriodicChecks = * Manually configured interval for periodic checks. */ | PeriodicChecksManualInterval; + +/* eslint-disable-next-line @typescript-eslint/no-namespace */ +export namespace ClusterClientConfiguration { + /** + * Enum representing pubsub subscription modes. + * See [Valkey PubSub Documentation](https://valkey.io/docs/topics/pubsub/) for more details. + */ + export enum PubSubChannelModes { + /** + * Use exact channel names. + */ + Exact = 0, + + /** + * Use channel name patterns. + */ + Pattern = 1, + + /** + * Use sharded pubsub. Available since Valkey version 7.0. + */ + Sharded = 2, + } + + export type PubSubSubscriptions = { + /** + * Channels and patterns by modes. + */ + channelsAndPatterns: Partial>>; + + /** + * Optional callback to accept the incoming messages. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + callback?: (msg: PubSubMsg, context: any) => void; + + /** + * Arbitrary context to pass to the callback. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + context?: any; + }; +} export type ClusterClientConfiguration = BaseClientConfiguration & { /** * Configure the periodic topology checks. @@ -56,6 +110,12 @@ export type ClusterClientConfiguration = BaseClientConfiguration & { * If not set, `enabledDefaultConfigs` will be used. */ periodicChecks?: PeriodicChecks; + + /** + * PubSub subscriptions to be used for the client. + * Will be applied via SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE commands during connection establishment. + */ + pubsubSubscriptions?: ClusterClientConfiguration.PubSubSubscriptions; }; /** @@ -130,48 +190,48 @@ export type SingleNodeRoute = function toProtobufRoute( route: Routes | undefined, -): redis_request.Routes | undefined { +): command_request.Routes | undefined { if (route === undefined) { return undefined; } if (route === "allPrimaries") { - return redis_request.Routes.create({ - simpleRoutes: redis_request.SimpleRoutes.AllPrimaries, + return command_request.Routes.create({ + simpleRoutes: command_request.SimpleRoutes.AllPrimaries, }); } else if (route === "allNodes") { - return redis_request.Routes.create({ - simpleRoutes: redis_request.SimpleRoutes.AllNodes, + return command_request.Routes.create({ + simpleRoutes: command_request.SimpleRoutes.AllNodes, }); } else if (route === "randomNode") { - return redis_request.Routes.create({ - simpleRoutes: redis_request.SimpleRoutes.Random, + return command_request.Routes.create({ + simpleRoutes: command_request.SimpleRoutes.Random, }); } else if (route.type === "primarySlotKey") { - return redis_request.Routes.create({ - slotKeyRoute: redis_request.SlotKeyRoute.create({ - slotType: redis_request.SlotTypes.Primary, + return command_request.Routes.create({ + slotKeyRoute: command_request.SlotKeyRoute.create({ + slotType: command_request.SlotTypes.Primary, slotKey: route.key, }), }); } else if (route.type === "replicaSlotKey") { - return redis_request.Routes.create({ - slotKeyRoute: redis_request.SlotKeyRoute.create({ - slotType: redis_request.SlotTypes.Replica, + return command_request.Routes.create({ + slotKeyRoute: command_request.SlotKeyRoute.create({ + slotType: command_request.SlotTypes.Replica, slotKey: route.key, }), }); } else if (route.type === "primarySlotId") { - return redis_request.Routes.create({ - slotKeyRoute: redis_request.SlotIdRoute.create({ - slotType: redis_request.SlotTypes.Primary, + return command_request.Routes.create({ + slotKeyRoute: command_request.SlotIdRoute.create({ + slotType: command_request.SlotTypes.Primary, slotId: route.id, }), }); } else if (route.type === "replicaSlotId") { - return redis_request.Routes.create({ - slotKeyRoute: redis_request.SlotIdRoute.create({ - slotType: redis_request.SlotTypes.Replica, + return command_request.Routes.create({ + slotKeyRoute: command_request.SlotIdRoute.create({ + slotType: command_request.SlotTypes.Replica, slotId: route.id, }), }); @@ -193,7 +253,7 @@ function toProtobufRoute( port = Number(split[1]); } - return redis_request.Routes.create({ + return command_request.Routes.create({ byAddressRoute: { host, port }, }); } @@ -202,9 +262,9 @@ function toProtobufRoute( /** * Client used for connection to cluster Redis servers. * For full documentation, see - * https://github.com/aws/babushka/wiki/NodeJS-wrapper#redis-cluster + * https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#cluster */ -export class RedisClusterClient extends BaseClient { +export class GlideClusterClient extends BaseClient { /** * @internal */ @@ -230,27 +290,28 @@ export class RedisClusterClient extends BaseClient { } } + this.configurePubsub(options, configuration); return configuration; } public static async createClient( options: ClusterClientConfiguration, - ): Promise { + ): Promise { return await super.createClientInternal( options, (socket: net.Socket, options?: ClusterClientConfiguration) => - new RedisClusterClient(socket, options), + new GlideClusterClient(socket, options), ); } static async __createClient( options: BaseClientConfiguration, connectedSocket: net.Socket, - ): Promise { + ): Promise { return super.__createClientInternal( options, connectedSocket, - (socket, options) => new RedisClusterClient(socket, options), + (socket, options) => new GlideClusterClient(socket, options), ); } @@ -259,7 +320,8 @@ export class RedisClusterClient extends BaseClient { * The command will be routed automatically based on the passed command's default request policy, unless `route` is provided, * in which case the client will route the command to the nodes defined by `route`. * - * @remarks - This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + * for details on the restrictions and limitations of the custom command API. * * @example * ```typescript @@ -289,14 +351,19 @@ export class RedisClusterClient extends BaseClient { transaction: ClusterTransaction, route?: SingleNodeRoute, ): Promise { - return this.createWritePromise( + return this.createWritePromise( transaction.commands, toProtobufRoute(route), - ); + ).then((result: ReturnType[] | null) => { + return this.processResultWithSetCommands( + result, + transaction.setCommandsIndexes, + ); + }); } /** Ping the Redis server. - * See https://redis.io/commands/ping/ for details. + * See https://valkey.io/commands/ping/ for details. * * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". @@ -327,7 +394,7 @@ export class RedisClusterClient extends BaseClient { } /** Get information and statistics about the Redis server. - * See https://redis.io/commands/info/ for details. + * See https://valkey.io/commands/info/ for details. * * @param options - A list of InfoSection values specifying which sections of information to retrieve. * When no parameter is provided, the default option is assumed. @@ -347,7 +414,7 @@ export class RedisClusterClient extends BaseClient { } /** Get the name of the connection to which the request is routed. - * See https://redis.io/commands/client-getname/ for more details. + * See https://valkey.io/commands/client-getname/ for more details. * * @param route - The command will be routed a random node, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -380,7 +447,7 @@ export class RedisClusterClient extends BaseClient { } /** Rewrite the configuration file with the current configuration. - * See https://redis.io/commands/config-rewrite/ for details. + * See https://valkey.io/commands/config-rewrite/ for details. * * @param route - The command will be routed to all nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -402,7 +469,7 @@ export class RedisClusterClient extends BaseClient { } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - * See https://redis.io/commands/config-resetstat/ for details. + * See https://valkey.io/commands/config-resetstat/ for details. * * @param route - The command will be routed to all nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -424,7 +491,7 @@ export class RedisClusterClient extends BaseClient { } /** Returns the current connection id. - * See https://redis.io/commands/client-id/ for details. + * See https://valkey.io/commands/client-id/ for details. * * @param route - The command will be routed to a random node, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -439,7 +506,7 @@ export class RedisClusterClient extends BaseClient { } /** Reads the configuration parameters of a running Redis server. - * See https://redis.io/commands/config-get/ for details. + * See https://valkey.io/commands/config-get/ for details. * * @param parameters - A list of configuration parameter names to retrieve values for. * @param route - The command will be routed to a random node, unless `route` is provided, in which @@ -474,7 +541,7 @@ export class RedisClusterClient extends BaseClient { } /** Set configuration parameters to the specified values. - * See https://redis.io/commands/config-set/ for details. + * See https://valkey.io/commands/config-set/ for details. * * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. * @param route - The command will be routed to all nodes, unless `route` is provided, in which @@ -501,7 +568,7 @@ export class RedisClusterClient extends BaseClient { } /** Echoes the provided `message` back. - * See https://redis.io/commands/echo for more details. + * See https://valkey.io/commands/echo for more details. * * @param message - The message to be echoed back. * @param route - The command will be routed to a random node, unless `route` is provided, in which @@ -512,14 +579,14 @@ export class RedisClusterClient extends BaseClient { * @example * ```typescript * // Example usage of the echo command - * const echoedMessage = await client.echo("Glide-for-Redis"); - * console.log(echoedMessage); // Output: "Glide-for-Redis" + * const echoedMessage = await client.echo("valkey-glide"); + * console.log(echoedMessage); // Output: "valkey-glide" * ``` * @example * ```typescript * // Example usage of the echo command with routing to all nodes - * const echoedMessage = await client.echo("Glide-for-Redis", "allNodes"); - * console.log(echoedMessage); // Output: {'addr': 'Glide-for-Redis', 'addr2': 'Glide-for-Redis', 'addr3': 'Glide-for-Redis'} + * const echoedMessage = await client.echo("valkey-glide", "allNodes"); + * console.log(echoedMessage); // Output: {'addr': 'valkey-glide', 'addr2': 'valkey-glide', 'addr3': 'valkey-glide'} * ``` */ public echo( @@ -533,7 +600,7 @@ export class RedisClusterClient extends BaseClient { } /** Returns the server time. - * See https://redis.io/commands/time/ for details. + * See https://valkey.io/commands/time/ for details. * * @param route - The command will be routed to a random node, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. @@ -561,4 +628,108 @@ export class RedisClusterClient extends BaseClient { public time(route?: Routes): Promise> { return this.createWritePromise(createTime(), toProtobufRoute(route)); } + + /** + * Displays a piece of generative computer art and the server version. + * + * See https://valkey.io/commands/lolwut/ for more details. + * + * @param options - The LOLWUT options. + * @param route - The command will be routed to a random node, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns A piece of generative computer art along with the current server version. + * + * @example + * ```typescript + * const response = await client.lolwut({ version: 6, parameters: [40, 20] }, "allNodes"); + * console.log(response); // Output: "Redis ver. 7.2.3" - Indicates the current server version. + * ``` + */ + public lolwut( + options?: LolwutOptions, + route?: Routes, + ): Promise> { + return this.createWritePromise( + createLolwut(options), + toProtobufRoute(route), + ); + } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * See https://valkey.io/commands/flushall/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * @param route - The command will be routed to all primaries, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns `OK`. + * + * @example + * ```typescript + * const result = await client.flushall(FlushMode.SYNC); + * console.log(result); // Output: 'OK' + * ``` + */ + public flushall(mode?: FlushMode, route?: Routes): Promise { + return this.createWritePromise( + createFlushAll(mode), + toProtobufRoute(route), + ); + } + + /** + * Returns the number of keys in the database. + * + * See https://valkey.io/commands/dbsize/ for more details. + + * @param route - The command will be routed to all primaries, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns The number of keys in the database. + * In the case of routing the query to multiple nodes, returns the aggregated number of keys across the different nodes. + * + * @example + * ```typescript + * const numKeys = await client.dbsize("allPrimaries"); + * console.log("Number of keys across all primary nodes: ", numKeys); + * ``` + */ + public dbsize(route?: Routes): Promise> { + return this.createWritePromise(createDBSize(), toProtobufRoute(route)); + } + + /** Publish a message on pubsub channel. + * This command aggregates PUBLISH and SPUBLISH commands functionalities. + * The mode is selected using the 'sharded' parameter. + * For both sharded and non-sharded mode, request is routed using hashed channel as key. + * See https://valkey.io/commands/publish and https://valkey.io/commands/spublish for more details. + * + * @param message - Message to publish. + * @param channel - Channel to publish the message on. + * @param sharded - Use sharded pubsub mode. Available since Valkey version 7.0. + * @returns - Number of subscriptions in primary node that received the message. + * + * @example + * ```typescript + * // Example usage of publish command + * const result = await client.publish("Hi all!", "global-channel"); + * console.log(result); // Output: 1 - This message was posted to 1 subscription which is configured on primary node + * ``` + * + * @example + * ```typescript + * // Example usage of spublish command + * const result = await client.publish("Hi all!", "global-channel", true); + * console.log(result); // Output: 2 - Published 2 instances of "Hi to sharded channel1!" message on channel1 using sharded mode + * ``` + */ + public publish( + message: string, + channel: string, + sharded: boolean = false, + ): Promise { + return this.createWritePromise( + createPublish(message, channel, sharded), + ); + } } diff --git a/node/src/Logger.ts b/node/src/Logger.ts index 28a2a7a334..4560e4d218 100644 --- a/node/src/Logger.ts +++ b/node/src/Logger.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { InitInternalLogger, Level, log } from "glide-rs"; diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 6b118f57d6..d2f637f77e 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1,10 +1,16 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { LPosOptions } from "./command-options/LPosOptions"; import { + AggregationType, ExpireOptions, + FlushMode, InfoOptions, + InsertPosition, + KeyWeight, + LolwutOptions, RangeByIndex, RangeByLex, RangeByScore, @@ -13,8 +19,9 @@ import { StreamAddOptions, StreamReadOptions, StreamTrimOptions, - ZaddOptions, - createBrpop, + ZAddOptions, + createBLPop, + createBRPop, createClientGetName, createClientId, createConfigGet, @@ -22,6 +29,7 @@ import { createConfigRewrite, createConfigSet, createCustomCommand, + createDBSize, createDecr, createDecrBy, createDel, @@ -29,6 +37,7 @@ import { createExists, createExpire, createExpireAt, + createFlushAll, createGet, createHDel, createHExists, @@ -40,58 +49,83 @@ import { createHMGet, createHSet, createHSetNX, - createHvals, + createHVals, createIncr, createIncrBy, createIncrByFloat, createInfo, + createLIndex, + createLInsert, createLLen, createLPop, + createLPos, createLPush, + createLPushX, createLRange, createLRem, + createLSet, createLTrim, - createLindex, + createLolwut, createMGet, createMSet, + createObjectEncoding, + createObjectFreq, + createObjectIdletime, + createObjectRefcount, createPExpire, createPExpireAt, + createPTTL, createPersist, + createPfAdd, + createPfCount, createPing, - createPttl, createRPop, createRPush, + createRPushX, createRename, + createRenameNX, createSAdd, createSCard, + createSDiff, + createSDiffStore, + createSInter, + createSInterCard, + createSInterStore, + createSIsMember, createSMembers, + createSMIsMember, + createSMove, createSPop, createSRem, + createSUnion, + createSUnionStore, createSelect, createSet, - createSismember, createStrlen, createTTL, createTime, createType, createUnlink, - createXadd, - createXread, - createXtrim, - createZadd, - createZcard, - createZcount, - createZpopmax, - createZpopmin, - createZrange, - createZrangeWithScores, - createZrank, - createZrem, - createZremRangeByRank, - createZremRangeByScore, - createZscore, + createXAdd, + createXLen, + createXRead, + createXTrim, + createZAdd, + createZCard, + createZCount, + createZInterCard, + createZInterstore, + createZPopMax, + createZPopMin, + createZRange, + createZRangeWithScores, + createZRank, + createZRem, + createZRemRangeByRank, + createZRemRangeByScore, + createZScore, } from "./Commands"; -import { redis_request } from "./ProtobufMessage"; +import { command_request } from "./ProtobufMessage"; /** * Base class encompassing shared commands for both standalone and cluster mode implementations in a transaction. @@ -104,25 +138,46 @@ import { redis_request } from "./ProtobufMessage"; * Specific response types are documented alongside each method. * * @example - * transaction = new BaseTransaction() - * .set("key", "value") - * .get("key"); - * await client.exec(transaction); - * [OK , "value"] + * ```typescript + * const transaction = new BaseTransaction() + * .set("key", "value") + * .get("key"); + * const result = await client.exec(transaction); + * console.log(result); // Output: ['OK', 'value'] + * ``` */ export class BaseTransaction> { /** * @internal */ - readonly commands: redis_request.Command[] = []; + readonly commands: command_request.Command[] = []; + /** + * Array of command indexes indicating commands that need to be converted into a `Set` within the transaction. + * @internal + */ + readonly setCommandsIndexes: number[] = []; + + /** + * Adds a command to the transaction and returns the transaction instance. + * @param command - The command to add. + * @param shouldConvertToSet - Indicates if the command should be converted to a `Set`. + * @returns The updated transaction instance. + */ + protected addAndReturn( + command: command_request.Command, + shouldConvertToSet: boolean = false, + ): T { + if (shouldConvertToSet) { + // The command's index within the transaction is saved for later conversion of its response to a Set type. + this.setCommandsIndexes.push(this.commands.length); + } - protected addAndReturn(command: redis_request.Command): T { this.commands.push(command); return this as unknown as T; } /** Get the value associated with the given key, or null if no such value exists. - * See https://redis.io/commands/get/ for details. + * See https://valkey.io/commands/get/ for details. * * @param key - The key to retrieve from the database. * @@ -133,7 +188,7 @@ export class BaseTransaction> { } /** Set the given key with the given value. Return value is dependent on the passed options. - * See https://redis.io/commands/set/ for details. + * See https://valkey.io/commands/set/ for details. * * @param key - The key to store. * @param value - The value to store with the given key. @@ -148,7 +203,7 @@ export class BaseTransaction> { } /** Ping the Redis server. - * See https://redis.io/commands/ping/ for details. + * See https://valkey.io/commands/ping/ for details. * * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". @@ -161,7 +216,7 @@ export class BaseTransaction> { } /** Get information and statistics about the Redis server. - * See https://redis.io/commands/info/ for details. + * See https://valkey.io/commands/info/ for details. * * @param options - A list of InfoSection values specifying which sections of information to retrieve. * When no parameter is provided, the default option is assumed. @@ -173,7 +228,7 @@ export class BaseTransaction> { } /** Remove the specified keys. A key is ignored if it does not exist. - * See https://redis.io/commands/del/ for details. + * See https://valkey.io/commands/del/ for details. * * @param keys - A list of keys to be deleted from the database. * @@ -184,7 +239,7 @@ export class BaseTransaction> { } /** Get the name of the connection on which the transaction is being executed. - * See https://redis.io/commands/client-getname/ for more details. + * See https://valkey.io/commands/client-getname/ for more details. * * Command Response - the name of the client connection as a string if a name is set, or null if no name is assigned. */ @@ -193,7 +248,7 @@ export class BaseTransaction> { } /** Rewrite the configuration file with the current configuration. - * See https://redis.io/commands/select/ for details. + * See https://valkey.io/commands/select/ for details. * * Command Response - "OK" when the configuration was rewritten properly. Otherwise, the transaction fails with an error. */ @@ -202,7 +257,7 @@ export class BaseTransaction> { } /** Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - * See https://redis.io/commands/config-resetstat/ for details. + * See https://valkey.io/commands/config-resetstat/ for details. * * Command Response - always "OK". */ @@ -211,7 +266,7 @@ export class BaseTransaction> { } /** Retrieve the values of multiple keys. - * See https://redis.io/commands/mget/ for details. + * See https://valkey.io/commands/mget/ for details. * * @param keys - A list of keys to retrieve values for. * @@ -223,7 +278,7 @@ export class BaseTransaction> { } /** Set multiple keys to multiple values in a single atomic operation. - * See https://redis.io/commands/mset/ for details. + * See https://valkey.io/commands/mset/ for details. * * @param keyValueMap - A key-value map consisting of keys and their respective values to set. * @@ -234,7 +289,7 @@ export class BaseTransaction> { } /** Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/incr/ for details. + * See https://valkey.io/commands/incr/ for details. * * @param key - The key to increment its value. * @@ -245,7 +300,7 @@ export class BaseTransaction> { } /** Increments the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/incrby/ for details. + * See https://valkey.io/commands/incrby/ for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -259,7 +314,7 @@ export class BaseTransaction> { /** Increment the string representing a floating point number stored at `key` by `amount`. * By using a negative amount value, the result is that the value stored at `key` is decremented. * If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/incrbyfloat/ for details. + * See https://valkey.io/commands/incrbyfloat/ for details. * * @param key - The key to increment its value. * @param amount - The amount to increment. @@ -272,7 +327,7 @@ export class BaseTransaction> { } /** Returns the current connection id. - * See https://redis.io/commands/client-id/ for details. + * See https://valkey.io/commands/client-id/ for details. * * Command Response - the id of the client. */ @@ -281,7 +336,7 @@ export class BaseTransaction> { } /** Decrements the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/decr/ for details. + * See https://valkey.io/commands/decr/ for details. * * @param key - The key to decrement its value. * @@ -292,7 +347,7 @@ export class BaseTransaction> { } /** Decrements the number stored at `key` by `amount`. If `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/decrby/ for details. + * See https://valkey.io/commands/decrby/ for details. * * @param key - The key to decrement its value. * @param amount - The amount to decrement. @@ -304,7 +359,7 @@ export class BaseTransaction> { } /** Reads the configuration parameters of a running Redis server. - * See https://redis.io/commands/config-get/ for details. + * See https://valkey.io/commands/config-get/ for details. * * @param parameters - A list of configuration parameter names to retrieve values for. * @@ -316,22 +371,18 @@ export class BaseTransaction> { } /** Set configuration parameters to the specified values. - * See https://redis.io/commands/config-set/ for details. + * See https://valkey.io/commands/config-set/ for details. * * @param parameters - A List of keyValuePairs consisting of configuration parameters and their respective values to set. * * Command Response - "OK" when the configuration was set properly. Otherwise, the transaction fails with an error. - * - * @example - * config_set([("timeout", "1000")], [("maxmemory", "1GB")]) - Returns OK - * */ public configSet(parameters: Record): T { return this.addAndReturn(createConfigSet(parameters)); } /** Retrieve the value associated with `field` in the hash stored at `key`. - * See https://redis.io/commands/hget/ for details. + * See https://valkey.io/commands/hget/ for details. * * @param key - The key of the hash. * @param field - The field in the hash stored at `key` to retrieve from the database. @@ -343,7 +394,7 @@ export class BaseTransaction> { } /** Sets the specified fields to their respective values in the hash stored at `key`. - * See https://redis.io/commands/hset/ for details. + * See https://valkey.io/commands/hset/ for details. * * @param key - The key of the hash. * @param fieldValueMap - A field-value map consisting of fields and their corresponding values @@ -358,7 +409,7 @@ export class BaseTransaction> { /** Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. * If `key` does not exist, a new key holding a hash is created. * If `field` already exists, this operation has no effect. - * See https://redis.io/commands/hsetnx/ for more details. + * See https://valkey.io/commands/hsetnx/ for more details. * * @param key - The key of the hash. * @param field - The field to set the value for. @@ -372,7 +423,7 @@ export class BaseTransaction> { /** Removes the specified fields from the hash stored at `key`. * Specified fields that do not exist within this hash are ignored. - * See https://redis.io/commands/hdel/ for details. + * See https://valkey.io/commands/hdel/ for details. * * @param key - The key of the hash. * @param fields - The fields to remove from the hash stored at `key`. @@ -385,7 +436,7 @@ export class BaseTransaction> { } /** Returns the values associated with the specified fields in the hash stored at `key`. - * See https://redis.io/commands/hmget/ for details. + * See https://valkey.io/commands/hmget/ for details. * * @param key - The key of the hash. * @param fields - The fields in the hash stored at `key` to retrieve from the database. @@ -399,7 +450,7 @@ export class BaseTransaction> { } /** Returns if `field` is an existing field in the hash stored at `key`. - * See https://redis.io/commands/hexists/ for details. + * See https://valkey.io/commands/hexists/ for details. * * @param key - The key of the hash. * @param field - The field to check in the hash stored at `key`. @@ -412,7 +463,7 @@ export class BaseTransaction> { } /** Returns all fields and values of the hash stored at `key`. - * See https://redis.io/commands/hgetall/ for details. + * See https://valkey.io/commands/hgetall/ for details. * * @param key - The key of the hash. * @@ -426,7 +477,7 @@ export class BaseTransaction> { /** Increments the number stored at `field` in the hash stored at `key` by `increment`. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/hincrby/ for details. + * See https://valkey.io/commands/hincrby/ for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -441,7 +492,7 @@ export class BaseTransaction> { /** Increment the string representing a floating point number stored at `field` in the hash stored at `key` by `increment`. * By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. * If `field` or `key` does not exist, it is set to 0 before performing the operation. - * See https://redis.io/commands/hincrbyfloat/ for details. + * See https://valkey.io/commands/hincrbyfloat/ for details. * * @param key - The key of the hash. * @param amount - The amount to increment. @@ -454,7 +505,7 @@ export class BaseTransaction> { } /** Returns the number of fields contained in the hash stored at `key`. - * See https://redis.io/commands/hlen/ for more details. + * See https://valkey.io/commands/hlen/ for more details. * * @param key - The key of the hash. * @@ -465,20 +516,20 @@ export class BaseTransaction> { } /** Returns all values in the hash stored at key. - * See https://redis.io/commands/hvals/ for more details. + * See https://valkey.io/commands/hvals/ for more details. * * @param key - The key of the hash. * * Command Response - a list of values in the hash, or an empty list when the key does not exist. */ public hvals(key: string): T { - return this.addAndReturn(createHvals(key)); + return this.addAndReturn(createHVals(key)); } /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://redis.io/commands/lpush/ for details. + * See https://valkey.io/commands/lpush/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the head of the list stored at `key`. @@ -489,9 +540,24 @@ export class BaseTransaction> { return this.addAndReturn(createLPush(key, elements)); } + /** + * Inserts specified values at the head of the `list`, only if `key` already + * exists and holds a list. + * + * See https://valkey.io/commands/lpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the head of the list stored at `key`. + * + * Command Response - The length of the list after the push operation. + */ + public lpushx(key: string, elements: string[]): T { + return this.addAndReturn(createLPushX(key, elements)); + } + /** Removes and returns the first elements of the list stored at `key`. * The command pops a single element from the beginning of the list. - * See https://redis.io/commands/lpop/ for details. + * See https://valkey.io/commands/lpop/ for details. * * @param key - The key of the list. * @@ -503,7 +569,7 @@ export class BaseTransaction> { } /** Removes and returns up to `count` elements of the list stored at `key`, depending on the list's length. - * See https://redis.io/commands/lpop/ for details. + * See https://valkey.io/commands/lpop/ for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -519,7 +585,7 @@ export class BaseTransaction> { * The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. * These offsets can also be negative numbers indicating offsets starting at the end of the list, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://redis.io/commands/lrange/ for details. + * See https://valkey.io/commands/lrange/ for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -535,7 +601,7 @@ export class BaseTransaction> { } /** Returns the length of the list stored at `key`. - * See https://redis.io/commands/llen/ for details. + * See https://valkey.io/commands/llen/ for details. * * @param key - The key of the list. * @@ -546,11 +612,29 @@ export class BaseTransaction> { return this.addAndReturn(createLLen(key)); } + /** + * Sets the list element at `index` to `element`. + * The index is zero-based, so `0` means the first element, `1` the second element and so on. + * Negative indices can be used to designate elements starting at the tail of + * the list. Here, `-1` means the last element, `-2` means the penultimate and so forth. + * + * See https://valkey.io/commands/lset/ for details. + * + * @param key - The key of the list. + * @param index - The index of the element in the list to be set. + * @param element - The new element to set at the specified index. + * + * Command Response - Always "OK". + */ + public lset(key: string, index: number, element: string): T { + return this.addAndReturn(createLSet(key, index, element)); + } + /** Trim an existing list so that it will contain only the specified range of elements specified. * The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. * These offsets can also be negative numbers indicating offsets starting at the end of the list, * with -1 being the last element of the list, -2 being the penultimate, and so on. - * See https://redis.io/commands/ltrim/ for details. + * See https://valkey.io/commands/ltrim/ for details. * * @param key - The key of the list. * @param start - The starting point of the range. @@ -584,7 +668,7 @@ export class BaseTransaction> { /** Inserts all the specified values at the tail of the list stored at `key`. * `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. - * See https://redis.io/commands/rpush/ for details. + * See https://valkey.io/commands/rpush/ for details. * * @param key - The key of the list. * @param elements - The elements to insert at the tail of the list stored at `key`. @@ -595,9 +679,24 @@ export class BaseTransaction> { return this.addAndReturn(createRPush(key, elements)); } + /** + * Inserts specified values at the tail of the `list`, only if `key` already + * exists and holds a list. + * + * See https://valkey.io/commands/rpushx/ for details. + * + * @param key - The key of the list. + * @param elements - The elements to insert at the tail of the list stored at `key`. + * + * Command Response - The length of the list after the push operation. + */ + public rpushx(key: string, elements: string[]): T { + return this.addAndReturn(createRPushX(key, elements)); + } + /** Removes and returns the last elements of the list stored at `key`. * The command pops a single element from the end of the list. - * See https://redis.io/commands/rpop/ for details. + * See https://valkey.io/commands/rpop/ for details. * * @param key - The key of the list. * @@ -609,7 +708,7 @@ export class BaseTransaction> { } /** Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. - * See https://redis.io/commands/rpop/ for details. + * See https://valkey.io/commands/rpop/ for details. * * @param key - The key of the list. * @param count - The count of the elements to pop from the list. @@ -623,7 +722,7 @@ export class BaseTransaction> { /** Adds the specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. * If `key` does not exist, a new set is created before adding `members`. - * See https://redis.io/commands/sadd/ for details. + * See https://valkey.io/commands/sadd/ for details. * * @param key - The key to store the members to its set. * @param members - A list of members to add to the set stored at `key`. @@ -635,7 +734,7 @@ export class BaseTransaction> { } /** Removes the specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. - * See https://redis.io/commands/srem/ for details. + * See https://valkey.io/commands/srem/ for details. * * @param key - The key to remove the members from its set. * @param members - A list of members to remove from the set stored at `key`. @@ -648,7 +747,7 @@ export class BaseTransaction> { } /** Returns all the members of the set value stored at `key`. - * See https://redis.io/commands/smembers/ for details. + * See https://valkey.io/commands/smembers/ for details. * * @param key - The key to return its members. * @@ -656,11 +755,25 @@ export class BaseTransaction> { * If `key` does not exist, it is treated as an empty set and this command returns empty list. */ public smembers(key: string): T { - return this.addAndReturn(createSMembers(key)); + return this.addAndReturn(createSMembers(key), true); + } + + /** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. + * Creates a new destination set if needed. The operation is atomic. + * See https://valkey.io/commands/smove for more details. + * + * @param source - The key of the set to remove the element from. + * @param destination - The key of the set to add the element to. + * @param member - The set element to move. + * + * Command Response - `true` on success, or `false` if the `source` set does not exist or the element is not a member of the source set. + */ + public smove(source: string, destination: string, member: string): T { + return this.addAndReturn(createSMove(source, destination, member)); } /** Returns the set cardinality (number of elements) of the set stored at `key`. - * See https://redis.io/commands/scard/ for details. + * See https://valkey.io/commands/scard/ for details. * * @param key - The key to return the number of its members. * @@ -670,8 +783,107 @@ export class BaseTransaction> { return this.addAndReturn(createSCard(key)); } + /** Gets the intersection of all the given sets. + * When in cluster mode, all `keys` must map to the same hash slot. + * See https://valkey.io/docs/latest/commands/sinter/ for more details. + * + * @param keys - The `keys` of the sets to get the intersection. + * + * Command Response - A set of members which are present in all given sets. + * If one or more sets do not exist, an empty set will be returned. + */ + public sinter(keys: string[]): T { + return this.addAndReturn(createSInter(keys), true); + } + + /** + * Gets the cardinality of the intersection of all the given sets. + * + * See https://valkey.io/commands/sintercard/ for more details. + * + * @param keys - The keys of the sets. + * + * Command Response - The cardinality of the intersection result. If one or more sets do not exist, `0` is returned. + * + * since Valkey version 7.0.0. + */ + public sintercard(keys: string[], limit?: number): T { + return this.addAndReturn(createSInterCard(keys, limit)); + } + + /** + * Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. + * + * See https://valkey.io/commands/sinterstore/ for more details. + * + * @param destination - The key of the destination set. + * @param keys - The keys from which to retrieve the set members. + * + * Command Response - The number of elements in the resulting set. + */ + public sinterstore(destination: string, keys: string[]): T { + return this.addAndReturn(createSInterStore(destination, keys)); + } + + /** + * Computes the difference between the first set and all the successive sets in `keys`. + * + * See https://valkey.io/commands/sdiff/ for more details. + * + * @param keys - The keys of the sets to diff. + * + * Command Response - A `Set` of elements representing the difference between the sets. + * If a key in `keys` does not exist, it is treated as an empty set. + */ + public sdiff(keys: string[]): T { + return this.addAndReturn(createSDiff(keys), true); + } + + /** + * Stores the difference between the first set and all the successive sets in `keys` into a new set at `destination`. + * + * See https://valkey.io/commands/sdiffstore/ for more details. + * + * @param destination - The key of the destination set. + * @param keys - The keys of the sets to diff. + * + * Command Response - The number of elements in the resulting set. + */ + public sdiffstore(destination: string, keys: string[]): T { + return this.addAndReturn(createSDiffStore(destination, keys)); + } + + /** + * Gets the union of all the given sets. + * + * See https://valkey.io/commands/sunion/ for more details. + * + * @param keys - The keys of the sets. + * + * Command Response - A `Set` of members which are present in at least one of the given sets. + * If none of the sets exist, an empty `Set` will be returned. + */ + public sunion(keys: string[]): T { + return this.addAndReturn(createSUnion(keys), true); + } + + /** + * Stores the members of the union of all given sets specified by `keys` into a new set + * at `destination`. + * + * See https://valkey.io/commands/sunionstore/ for details. + * + * @param destination - The key of the destination set. + * @param keys - The keys from which to retrieve the set members. + * + * Command Response - The number of elements in the resulting set. + */ + public sunionstore(destination: string, keys: string[]): T { + return this.addAndReturn(createSUnionStore(destination, keys)); + } + /** Returns if `member` is a member of the set stored at `key`. - * See https://redis.io/commands/sismember/ for more details. + * See https://valkey.io/commands/sismember/ for more details. * * @param key - The key of the set. * @param member - The member to check for existence in the set. @@ -680,11 +892,27 @@ export class BaseTransaction> { * If `key` doesn't exist, it is treated as an empty set and the command returns `false`. */ public sismember(key: string, member: string): T { - return this.addAndReturn(createSismember(key, member)); + return this.addAndReturn(createSIsMember(key, member)); + } + + /** + * Checks whether each member is contained in the members of the set stored at `key`. + * + * See https://valkey.io/commands/smismember/ for more details. + * + * @param key - The key of the set to check. + * @param members - A list of members to check for existence in the set. + * + * Command Response - An `array` of `boolean` values, each indicating if the respective member exists in the set. + * + * since Valkey version 6.2.0. + */ + public smismember(key: string, members: string[]): T { + return this.addAndReturn(createSMIsMember(key, members)); } /** Removes and returns one random member from the set value store at `key`. - * See https://redis.io/commands/spop/ for details. + * See https://valkey.io/commands/spop/ for details. * To pop multiple members, see `spopCount`. * * @param key - The key of the set. @@ -697,7 +925,7 @@ export class BaseTransaction> { } /** Removes and returns up to `count` random members from the set value store at `key`, depending on the set's length. - * See https://redis.io/commands/spop/ for details. + * See https://valkey.io/commands/spop/ for details. * * @param key - The key of the set. * @param count - The count of the elements to pop from the set. @@ -706,11 +934,11 @@ export class BaseTransaction> { * If `key` does not exist, empty list will be returned. */ public spopCount(key: string, count: number): T { - return this.addAndReturn(createSPop(key, count)); + return this.addAndReturn(createSPop(key, count), true); } /** Returns the number of keys in `keys` that exist in the database. - * See https://redis.io/commands/exists/ for details. + * See https://valkey.io/commands/exists/ for details. * * @param keys - The keys list to check. * @@ -723,8 +951,8 @@ export class BaseTransaction> { /** Removes the specified keys. A key is ignored if it does not exist. * This command, similar to DEL, removes specified keys and ignores non-existent ones. - * However, this command does not block the server, while [DEL](https://redis.io/commands/del) does. - * See https://redis.io/commands/unlink/ for details. + * However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. + * See https://valkey.io/commands/unlink/ for details. * * @param keys - The keys we wanted to unlink. * @@ -738,7 +966,7 @@ export class BaseTransaction> { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `seconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/expire/ for details. + * See https://valkey.io/commands/expire/ for details. * * @param key - The key to set timeout on it. * @param seconds - The timeout in seconds. @@ -755,7 +983,7 @@ export class BaseTransaction> { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/expireat/ for details. + * See https://valkey.io/commands/expireat/ for details. * * @param key - The key to set timeout on it. * @param unixSeconds - The timeout in an absolute Unix timestamp. @@ -776,7 +1004,7 @@ export class BaseTransaction> { * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/pexpire/ for details. + * See https://valkey.io/commands/pexpire/ for details. * * @param key - The key to set timeout on it. * @param milliseconds - The timeout in milliseconds. @@ -797,7 +1025,7 @@ export class BaseTransaction> { * A timestamp in the past will delete the key immediately. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - * See https://redis.io/commands/pexpireat/ for details. + * See https://valkey.io/commands/pexpireat/ for details. * * @param key - The key to set timeout on it. * @param unixMilliseconds - The timeout in an absolute Unix timestamp. @@ -817,7 +1045,7 @@ export class BaseTransaction> { } /** Returns the remaining time to live of `key` that has a timeout. - * See https://redis.io/commands/ttl/ for details. + * See https://valkey.io/commands/ttl/ for details. * * @param key - The key to return its timeout. * @@ -829,11 +1057,11 @@ export class BaseTransaction> { /** Adds members with their scores to the sorted set stored at `key`. * If a member is already a part of the sorted set, its score is updated. - * See https://redis.io/commands/zadd/ for more details. + * See https://valkey.io/commands/zadd/ for more details. * * @param key - The key of the sorted set. * @param membersScoresMap - A mapping of members to their corresponding scores. - * @param options - The Zadd options. + * @param options - The ZAdd options. * @param changed - Modify the return value from the number of new elements added, to the total number of elements changed. * * Command Response - The number of elements added to the sorted set. @@ -842,11 +1070,11 @@ export class BaseTransaction> { public zadd( key: string, membersScoresMap: Record, - options?: ZaddOptions, + options?: ZAddOptions, changed?: boolean, ): T { return this.addAndReturn( - createZadd( + createZAdd( key, membersScoresMap, options, @@ -858,12 +1086,12 @@ export class BaseTransaction> { /** Increments the score of member in the sorted set stored at `key` by `increment`. * If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). * If `key` does not exist, a new sorted set with the specified member as its sole member is created. - * See https://redis.io/commands/zadd/ for more details. + * See https://valkey.io/commands/zadd/ for more details. * * @param key - The key of the sorted set. * @param member - A member in the sorted set to increment. * @param increment - The score to increment the member. - * @param options - The Zadd options. + * @param options - The ZAdd options. * * Command Response - The score of the member. * If there was a conflict with the options, the operation aborts and null is returned. @@ -872,16 +1100,16 @@ export class BaseTransaction> { key: string, member: string, increment: number, - options?: ZaddOptions, + options?: ZAddOptions, ): T { return this.addAndReturn( - createZadd(key, { [member]: increment }, options, "INCR"), + createZAdd(key, { [member]: increment }, options, "INCR"), ); } /** Removes the specified members from the sorted set stored at `key`. * Specified members that are not a member of this set are ignored. - * See https://redis.io/commands/zrem/ for more details. + * See https://valkey.io/commands/zrem/ for more details. * * @param key - The key of the sorted set. * @param members - A list of members to remove from the sorted set. @@ -890,11 +1118,11 @@ export class BaseTransaction> { * If `key` does not exist, it is treated as an empty sorted set, and this command returns 0. */ public zrem(key: string, members: string[]): T { - return this.addAndReturn(createZrem(key, members)); + return this.addAndReturn(createZRem(key, members)); } /** Returns the cardinality (number of elements) of the sorted set stored at `key`. - * See https://redis.io/commands/zcard/ for more details. + * See https://valkey.io/commands/zcard/ for more details. * * @param key - The key of the sorted set. * @@ -902,11 +1130,28 @@ export class BaseTransaction> { * If `key` does not exist, it is treated as an empty sorted set, and this command returns 0. */ public zcard(key: string): T { - return this.addAndReturn(createZcard(key)); + return this.addAndReturn(createZCard(key)); + } + + /** + * Returns the cardinality of the intersection of the sorted sets specified by `keys`. + * + * See https://valkey.io/commands/zintercard/ for more details. + * + * @param keys - The keys of the sorted sets to intersect. + * @param limit - An optional argument that can be used to specify a maximum number for the + * intersection cardinality. If limit is not supplied, or if it is set to `0`, there will be no limit. + * + * Command Response - The cardinality of the intersection of the given sorted sets. + * + * since - Redis version 7.0.0. + */ + public zintercard(keys: string[], limit?: number): T { + return this.addAndReturn(createZInterCard(keys, limit)); } /** Returns the score of `member` in the sorted set stored at `key`. - * See https://redis.io/commands/zscore/ for more details. + * See https://valkey.io/commands/zscore/ for more details. * * @param key - The key of the sorted set. * @param member - The member whose score is to be retrieved. @@ -916,11 +1161,11 @@ export class BaseTransaction> { * If `key` does not exist, null is returned. */ public zscore(key: string, member: string): T { - return this.addAndReturn(createZscore(key, member)); + return this.addAndReturn(createZScore(key, member)); } /** Returns the number of members in the sorted set stored at `key` with scores between `minScore` and `maxScore`. - * See https://redis.io/commands/zcount/ for more details. + * See https://valkey.io/commands/zcount/ for more details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to count from. Can be positive/negative infinity, or specific score and inclusivity. @@ -935,13 +1180,13 @@ export class BaseTransaction> { minScore: ScoreBoundary, maxScore: ScoreBoundary, ): T { - return this.addAndReturn(createZcount(key, minScore, maxScore)); + return this.addAndReturn(createZCount(key, minScore, maxScore)); } /** Returns the specified range of elements in the sorted set stored at `key`. * ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. * - * See https://redis.io/commands/zrange/ for more details. + * See https://valkey.io/commands/zrange/ for more details. * To get the elements with their scores, see `zrangeWithScores`. * * @param key - The key of the sorted set. @@ -959,12 +1204,12 @@ export class BaseTransaction> { rangeQuery: RangeByScore | RangeByLex | RangeByIndex, reverse: boolean = false, ): T { - return this.addAndReturn(createZrange(key, rangeQuery, reverse)); + return this.addAndReturn(createZRange(key, rangeQuery, reverse)); } /** Returns the specified range of elements with their scores in the sorted set stored at `key`. * Similar to ZRANGE but with a WITHSCORE flag. - * See https://redis.io/commands/zrange/ for more details. + * See https://valkey.io/commands/zrange/ for more details. * * @param key - The key of the sorted set. * @param rangeQuery - The range query object representing the type of range query to perform. @@ -982,12 +1227,37 @@ export class BaseTransaction> { reverse: boolean = false, ): T { return this.addAndReturn( - createZrangeWithScores(key, rangeQuery, reverse), + createZRangeWithScores(key, rangeQuery, reverse), + ); + } + + /** + * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + * + * See https://valkey.io/commands/zinterstore/ for more details. + * + * @param destination - The key of the destination sorted set. + * @param keys - The keys of the sorted sets with possible formats: + * string[] - for keys only. + * KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * Command Response - The number of elements in the resulting sorted set stored at `destination`. + */ + public zinterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn( + createZInterstore(destination, keys, aggregationType), ); } /** Returns the string representation of the type of the value stored at `key`. - * See https://redis.io/commands/type/ for more details. + * See https://valkey.io/commands/type/ for more details. * * @param key - The key to check its data type. * @@ -998,7 +1268,7 @@ export class BaseTransaction> { } /** Returns the length of the string value stored at `key`. - * See https://redis.io/commands/strlen/ for more details. + * See https://valkey.io/commands/strlen/ for more details. * * @param key - The `key` to check its length. * @@ -1012,7 +1282,7 @@ export class BaseTransaction> { /** Removes and returns the members with the lowest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the lowest scores are removed and returned. * Otherwise, only one member with the lowest score is removed and returned. - * See https://redis.io/commands/zpopmin for more details. + * See https://valkey.io/commands/zpopmin for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -1022,13 +1292,13 @@ export class BaseTransaction> { * If `count` is higher than the sorted set's cardinality, returns all members and their scores. */ public zpopmin(key: string, count?: number): T { - return this.addAndReturn(createZpopmin(key, count)); + return this.addAndReturn(createZPopMin(key, count)); } /** Removes and returns the members with the highest scores from the sorted set stored at `key`. * If `count` is provided, up to `count` members with the highest scores are removed and returned. * Otherwise, only one member with the highest score is removed and returned. - * See https://redis.io/commands/zpopmax for more details. + * See https://valkey.io/commands/zpopmax for more details. * * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. @@ -1038,11 +1308,11 @@ export class BaseTransaction> { * If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. */ public zpopmax(key: string, count?: number): T { - return this.addAndReturn(createZpopmax(key, count)); + return this.addAndReturn(createZPopMax(key, count)); } /** Echoes the provided `message` back. - * See https://redis.io/commands/echo for more details. + * See https://valkey.io/commands/echo for more details. * * @param message - The message to be echoed back. * @@ -1053,20 +1323,20 @@ export class BaseTransaction> { } /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. - * See https://redis.io/commands/pttl for more details. + * See https://valkey.io/commands/pttl for more details. * * @param key - The key to return its timeout. * * Command Response - TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. */ public pttl(key: string): T { - return this.addAndReturn(createPttl(key)); + return this.addAndReturn(createPTTL(key)); } /** Removes all elements in the sorted set stored at `key` with rank between `start` and `end`. * Both `start` and `end` are zero-based indexes with 0 being the element with the lowest score. * These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. - * See https://redis.io/commands/zremrangebyrank/ for more details. + * See https://valkey.io/commands/zremrangebyrank/ for more details. * * @param key - The key of the sorted set. * @param start - The starting point of the range. @@ -1078,11 +1348,11 @@ export class BaseTransaction> { * If `key` does not exist 0 will be returned. */ public zremRangeByRank(key: string, start: number, end: number): T { - return this.addAndReturn(createZremRangeByRank(key, start, end)); + return this.addAndReturn(createZRemRangeByRank(key, start, end)); } /** Removes all elements in the sorted set stored at `key` with a score between `minScore` and `maxScore`. - * See https://redis.io/commands/zremrangebyscore/ for more details. + * See https://valkey.io/commands/zremrangebyscore/ for more details. * * @param key - The key of the sorted set. * @param minScore - The minimum score to remove from. Can be positive/negative infinity, or specific score and inclusivity. @@ -1098,12 +1368,12 @@ export class BaseTransaction> { maxScore: ScoreBoundary, ): T { return this.addAndReturn( - createZremRangeByScore(key, minScore, maxScore), + createZRemRangeByScore(key, minScore, maxScore), ); } /** Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. - * See https://redis.io/commands/zrank for more details. + * See https://valkey.io/commands/zrank for more details. * To get the rank of `member` with its score, see `zrankWithScore`. * * @param key - The key of the sorted set. @@ -1113,11 +1383,11 @@ export class BaseTransaction> { * If `key` doesn't exist, or if `member` is not present in the set, null will be returned. */ public zrank(key: string, member: string): T { - return this.addAndReturn(createZrank(key, member)); + return this.addAndReturn(createZRank(key, member)); } /** Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. - * See https://redis.io/commands/zrank for more details. + * See https://valkey.io/commands/zrank for more details. * * @param key - The key of the sorted set. * @param member - The member whose rank is to be retrieved. @@ -1128,12 +1398,12 @@ export class BaseTransaction> { * since - Redis version 7.2.0. */ public zrankWithScore(key: string, member: string): T { - return this.addAndReturn(createZrank(key, member, true)); + return this.addAndReturn(createZRank(key, member, true)); } /** Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to * persistent (a key that will never expire as no timeout is associated). - * See https://redis.io/commands/persist/ for more details. + * See https://valkey.io/commands/persist/ for more details. * * @param key - The key to remove the existing timeout on. * @@ -1146,13 +1416,10 @@ export class BaseTransaction> { /** Executes a single command, without checking inputs. Every part of the command, including subcommands, * should be added as a separate value in args. * - * @remarks - This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + * See the [Glide for Redis Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + * for details on the restrictions and limitations of the custom command API. * - * @example - * Returns a list of all pub/sub clients: - * ```ts - * connection.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"]) - * ``` + * Command Response - A response from Redis with an `Object`. */ public customCommand(args: string[]): T { return this.addAndReturn(createCustomCommand(args)); @@ -1162,7 +1429,7 @@ export class BaseTransaction> { * The index is zero-based, so 0 means the first element, 1 the second element and so on. * Negative indices can be used to designate elements starting at the tail of the list. * Here, -1 means the last element, -2 means the penultimate and so forth. - * See https://redis.io/commands/lindex/ for more details. + * See https://valkey.io/commands/lindex/ for more details. * * @param key - The `key` of the list. * @param index - The `index` of the element in the list to retrieve. @@ -1170,12 +1437,36 @@ export class BaseTransaction> { * If `index` is out of range or if `key` does not exist, null is returned. */ public lindex(key: string, index: number): T { - return this.addAndReturn(createLindex(key, index)); + return this.addAndReturn(createLIndex(key, index)); + } + + /** + * Inserts `element` in the list at `key` either before or after the `pivot`. + * + * See https://valkey.io/commands/linsert/ for more details. + * + * @param key - The key of the list. + * @param position - The relative position to insert into - either `InsertPosition.Before` or + * `InsertPosition.After` the `pivot`. + * @param pivot - An element of the list. + * @param element - The new element to insert. + * + * Command Response - The list length after a successful insert operation. + * If the `key` doesn't exist returns `-1`. + * If the `pivot` wasn't found, returns `0`. + */ + public linsert( + key: string, + position: InsertPosition, + pivot: string, + element: string, + ): T { + return this.addAndReturn(createLInsert(key, position, pivot, element)); } /** * Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. - * See https://redis.io/commands/xadd/ for more details. + * See https://valkey.io/commands/xadd/ for more details. * * @param key - The key of the stream. * @param values - field-value pairs to be added to the entry. @@ -1186,23 +1477,23 @@ export class BaseTransaction> { values: [string, string][], options?: StreamAddOptions, ): T { - return this.addAndReturn(createXadd(key, values, options)); + return this.addAndReturn(createXAdd(key, values, options)); } /** * Trims the stream stored at `key` by evicting older entries. - * See https://redis.io/commands/xtrim/ for more details. + * See https://valkey.io/commands/xtrim/ for more details. * * @param key - the key of the stream * @param options - options detailing how to trim the stream. * @returns The number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. */ public xtrim(key: string, options: StreamTrimOptions): T { - return this.addAndReturn(createXtrim(key, options)); + return this.addAndReturn(createXTrim(key, options)); } /** Returns the server time. - * See https://redis.io/commands/time/ for details. + * See https://valkey.io/commands/time/ for details. * * @returns - The current server time as a two items `array`: * A Unix timestamp and the amount of microseconds already elapsed in the current second. @@ -1214,7 +1505,7 @@ export class BaseTransaction> { /** * Reads entries from the given streams. - * See https://redis.io/commands/xread/ for more details. + * See https://valkey.io/commands/xread/ for more details. * * @param keys_and_ids - pairs of keys and entry ids to read from. A pair is composed of a stream's key and the id of the entry after which the stream will be read. * @param options - options detailing how to read the stream. @@ -1224,7 +1515,20 @@ export class BaseTransaction> { keys_and_ids: Record, options?: StreamReadOptions, ): T { - return this.addAndReturn(createXread(keys_and_ids, options)); + return this.addAndReturn(createXRead(keys_and_ids, options)); + } + + /** + * Returns the number of entries in the stream stored at `key`. + * + * See https://valkey.io/commands/xlen/ for more details. + * + * @param key - The key of the stream. + * + * Command Response - The number of entries in the stream. If `key` does not exist, returns `0`. + */ + public xlen(key: string): T { + return this.addAndReturn(createXLen(key)); } /** @@ -1232,7 +1536,7 @@ export class BaseTransaction> { * If `newkey` already exists it is overwritten. * In Cluster mode, both `key` and `newkey` must be in the same hash slot, * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. - * See https://redis.io/commands/rename/ for more details. + * See https://valkey.io/commands/rename/ for more details. * * @param key - The key to rename. * @param newKey - The new name of the key. @@ -1242,21 +1546,188 @@ export class BaseTransaction> { return this.addAndReturn(createRename(key, newKey)); } + /** + * Renames `key` to `newkey` if `newkey` does not yet exist. + * In Cluster mode, both `key` and `newkey` must be in the same hash slot, + * meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. + * See https://valkey.io/commands/renamenx/ for more details. + * + * @param key - The key to rename. + * @param newKey - The new name of the key. + * Command Response - If the `key` was successfully renamed, returns `true`. Otherwise, returns `false`. + * If `key` does not exist, an error is thrown. + */ + public renamenx(key: string, newKey: string): T { + return this.addAndReturn(createRenameNX(key, newKey)); + } + /** Blocking list pop primitive. * Pop an element from the tail of the first list that is non-empty, - * with the given keys being checked in the order that they are given. + * with the given `keys` being checked in the order that they are given. * Blocks the connection when there are no elements to pop from any of the given lists. - * See https://redis.io/commands/brpop/ for more details. - * Note: BRPOP is a blocking command, - * see [Blocking Commands](https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands) for more details and best practices. + * See https://valkey.io/commands/brpop/ for more details. + * Note: `BRPOP` is a blocking command, + * see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. * * @param keys - The `keys` of the lists to pop from. * @param timeout - The `timeout` in seconds. * Command Response - An `array` containing the `key` from which the element was popped and the value of the popped element, - * formatted as [key, value]. If no element could be popped and the timeout expired, returns Null. + * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. */ public brpop(keys: string[], timeout: number): T { - return this.addAndReturn(createBrpop(keys, timeout)); + return this.addAndReturn(createBRPop(keys, timeout)); + } + + /** Blocking list pop primitive. + * Pop an element from the head of the first list that is non-empty, + * with the given `keys` being checked in the order that they are given. + * Blocks the connection when there are no elements to pop from any of the given lists. + * See https://valkey.io/commands/blpop/ for more details. + * Note: `BLPOP` is a blocking command, + * see [Blocking Commands](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands) for more details and best practices. + * + * @param keys - The `keys` of the lists to pop from. + * @param timeout - The `timeout` in seconds. + * Command Response - An `array` containing the `key` from which the element was popped and the value of the popped element, + * formatted as [key, value]. If no element could be popped and the timeout expired, returns `null`. + */ + public blpop(keys: string[], timeout: number): T { + return this.addAndReturn(createBLPop(keys, timeout)); + } + + /** Adds all elements to the HyperLogLog data structure stored at the specified `key`. + * Creates a new structure if the `key` does not exist. + * When no elements are provided, and `key` exists and is a HyperLogLog, then no operation is performed. + * + * See https://valkey.io/commands/pfadd/ for more details. + * + * @param key - The key of the HyperLogLog data structure to add elements into. + * @param elements - An array of members to add to the HyperLogLog stored at `key`. + * Command Response - If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is + * altered, then returns `1`. Otherwise, returns `0`. + */ + public pfadd(key: string, elements: string[]): T { + return this.addAndReturn(createPfAdd(key, elements)); + } + + /** Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or + * calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + * + * See https://valkey.io/commands/pfcount/ for more details. + * + * @param keys - The keys of the HyperLogLog data structures to be analyzed. + * Command Response - The approximated cardinality of given HyperLogLog data structures. + * The cardinality of a key that does not exist is `0`. + */ + public pfcount(keys: string[]): T { + return this.addAndReturn(createPfCount(keys)); + } + + /** Returns the internal encoding for the Redis object stored at `key`. + * + * See https://valkey.io/commands/object-encoding for more details. + * + * @param key - The `key` of the object to get the internal encoding of. + * Command Response - If `key` exists, returns the internal encoding of the object stored at `key` as a string. + * Otherwise, returns None. + */ + public objectEncoding(key: string): T { + return this.addAndReturn(createObjectEncoding(key)); + } + + /** Returns the logarithmic access frequency counter of a Redis object stored at `key`. + * + * See https://valkey.io/commands/object-freq for more details. + * + * @param key - The `key` of the object to get the logarithmic access frequency counter of. + * Command Response - If `key` exists, returns the logarithmic access frequency counter of + * the object stored at `key` as a `number`. Otherwise, returns `null`. + */ + public objectFreq(key: string): T { + return this.addAndReturn(createObjectFreq(key)); + } + + /** + * Returns the time in seconds since the last access to the value stored at `key`. + * + * See https://valkey.io/commands/object-idletime/ for more details. + * + * @param key - The key of the object to get the idle time of. + * + * Command Response - If `key` exists, returns the idle time in seconds. Otherwise, returns `null`. + */ + public objectIdletime(key: string): T { + return this.addAndReturn(createObjectIdletime(key)); + } + + /** + * Returns the reference count of the object stored at `key`. + * + * See https://valkey.io/commands/object-refcount/ for more details. + * + * @param key - The `key` of the object to get the reference count of. + * + * Command Response - If `key` exists, returns the reference count of the object stored at `key` as a `number`. + * Otherwise, returns `null`. + */ + public objectRefcount(key: string): T { + return this.addAndReturn(createObjectRefcount(key)); + } + + /** + * Displays a piece of generative computer art and the server version. + * + * See https://valkey.io/commands/lolwut/ for more details. + * + * @param options - The LOLWUT options. + * + * Command Response - A piece of generative computer art along with the current server version. + */ + public lolwut(options?: LolwutOptions): T { + return this.addAndReturn(createLolwut(options)); + } + + /** + * Deletes all the keys of all the existing databases. This command never fails. + * + * See https://valkey.io/commands/flushall/ for more details. + * + * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. + * Command Response - `OK`. + */ + public flushall(mode?: FlushMode): T { + return this.addAndReturn(createFlushAll(mode)); + } + + /** + * Returns the index of the first occurrence of `element` inside the list specified by `key`. If no + * match is found, `null` is returned. If the `count` option is specified, then the function returns + * an `array` of indices of matching elements within the list. + * + * See https://valkey.io/commands/lpos/ for more details. + * + * @param key - The name of the list. + * @param element - The value to search for within the list. + * @param options - The LPOS options. + * + * Command Response - The index of `element`, or `null` if `element` is not in the list. If the `count` + * option is specified, then the function returns an `array` of indices of matching elements within the list. + * + * since - Valkey version 6.0.6. + */ + public lpos(key: string, element: string, options?: LPosOptions): T { + return this.addAndReturn(createLPos(key, element, options)); + } + + /** + * Returns the number of keys in the currently selected database. + * + * See https://valkey.io/commands/dbsize/ for more details. + * + * Command Response - The number of keys in the currently selected database. + */ + public dbsize(): T { + return this.addAndReturn(createDBSize()); } } @@ -1265,24 +1736,26 @@ export class BaseTransaction> { * Transactions allow the execution of a group of commands in a single step. * * Command Response: - * An array of command responses is returned by the RedisClient.exec command, in the order they were given. + * An array of command responses is returned by the GlideClient.exec command, in the order they were given. * Each element in the array represents a command given to the transaction. * The response for each command depends on the executed Redis command. * Specific response types are documented alongside each method. * * @example - * transaction = new Transaction() - * .set("key", "value") - * .select(1) /// Standalone command - * .get("key"); - * await RedisClient.exec(transaction); - * [OK , OK , null] + * ```typescript + * const transaction = new Transaction() + * .set("key", "value") + * .select(1) /// Standalone command + * .get("key"); + * const result = await GlideClient.exec(transaction); + * console.log(result); // Output: ['OK', 'OK', null] + * ``` */ export class Transaction extends BaseTransaction { /// TODO: add MOVE, SLAVEOF and all SENTINEL commands /** Change the currently selected Redis database. - * See https://redis.io/commands/select/ for details. + * See https://valkey.io/commands/select/ for details. * * @param index - The index of the database to select. * @@ -1298,7 +1771,7 @@ export class Transaction extends BaseTransaction { * Transactions allow the execution of a group of commands in a single step. * * Command Response: - * An array of command responses is returned by the RedisClusterClient.exec command, in the order they were given. + * An array of command responses is returned by the GlideClusterClient.exec command, in the order they were given. * Each element in the array represents a command given to the transaction. * The response for each command depends on the executed Redis command. * Specific response types are documented alongside each method. diff --git a/node/src/command-options/LPosOptions.ts b/node/src/command-options/LPosOptions.ts new file mode 100644 index 0000000000..de2c0bcc2a --- /dev/null +++ b/node/src/command-options/LPosOptions.ts @@ -0,0 +1,64 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +/** + * Optional arguments to LPOS command. + * + * See https://valkey.io/commands/lpos/ for more details. + */ +export class LPosOptions { + /** Redis API keyword use to determine the rank of the match to return. */ + public static RANK_REDIS_API = "RANK"; + /** Redis API keyword used to extract specific number of matching indices from a list. */ + public static COUNT_REDIS_API = "COUNT"; + /** Redis API keyword used to determine the maximum number of list items to compare. */ + public static MAXLEN_REDIS_API = "MAXLEN"; + /** The rank of the match to return. */ + private rank?: number; + /** The specific number of matching indices from a list. */ + private count?: number; + /** The maximum number of comparisons to make between the element and the items in the list. */ + private maxLength?: number; + + constructor({ + rank, + count, + maxLength, + }: { + rank?: number; + count?: number; + maxLength?: number; + }) { + this.rank = rank; + this.count = count; + this.maxLength = maxLength; + } + + /** + * + * Converts LPosOptions into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + const args: string[] = []; + + if (this.rank !== undefined) { + args.push(LPosOptions.RANK_REDIS_API); + args.push(this.rank.toString()); + } + + if (this.count !== undefined) { + args.push(LPosOptions.COUNT_REDIS_API); + args.push(this.count.toString()); + } + + if (this.maxLength !== undefined) { + args.push(LPosOptions.MAXLEN_REDIS_API); + args.push(this.maxLength.toString()); + } + + return args; + } +} diff --git a/node/tests/AsyncClient.test.ts b/node/tests/AsyncClient.test.ts index 22f7ec5075..ec75809878 100644 --- a/node/tests/AsyncClient.test.ts +++ b/node/tests/AsyncClient.test.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { afterAll, afterEach, beforeAll, describe } from "@jest/globals"; diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 2993573393..16e2572dae 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { @@ -13,33 +13,51 @@ import { import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; import { - BaseClientConfiguration, + GlideClient, + GlideClientConfiguration, ProtocolVersion, - RedisClient, + PubSubMsg, Transaction, } from ".."; -import { redis_request } from "../src/ProtobufMessage"; +import { RedisCluster } from "../../utils/TestUtils.js"; +import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; -import { RedisCluster, flushallOnPort, transactionTest } from "./TestUtilities"; +import { + checkSimple, + convertStringArrayToBuffer, + flushAndCloseClient, + getClientConfigurationOption, + intoString, + parseCommandLineArgs, + parseEndpoints, + transactionTest, +} from "./TestUtilities"; + /* eslint-disable @typescript-eslint/no-var-requires */ type Context = { - client: RedisClient; + client: GlideClient; }; -const TIMEOUT = 10000; +const TIMEOUT = 50000; -describe("RedisClient", () => { +describe("GlideClient", () => { let testsFailed = 0; let cluster: RedisCluster; - let port: number; + let client: GlideClient; beforeAll(async () => { - cluster = await RedisCluster.createCluster(false, 1, 1); - port = cluster.ports()[0]; + const standaloneAddresses = + parseCommandLineArgs()["standalone-endpoints"]; + // Connect to cluster or create a new one based on the parsed addresses + cluster = standaloneAddresses + ? RedisCluster.initFromExistingCluster( + parseEndpoints(standaloneAddresses), + ) + : await RedisCluster.createCluster(false, 1, 1); }, 20000); afterEach(async () => { - await flushallOnPort(cluster.ports()[0]); + await flushAndCloseClient(false, cluster.getAddresses(), client); }); afterAll(async () => { @@ -48,20 +66,6 @@ describe("RedisClient", () => { } }, TIMEOUT); - const getAddress = (port: number) => { - return [{ host: "localhost", port }]; - }; - - const getOptions = ( - port: number, - protocol: ProtocolVersion, - ): BaseClientConfiguration => { - return { - addresses: getAddress(port), - protocol, - }; - }; - it("test protobuf encode/decode delimited", () => { // This test is required in order to verify that the autogenerated protobuf // files has been corrected and the encoding/decoding works as expected. @@ -71,8 +75,8 @@ describe("RedisClient", () => { callbackIdx: 1, singleCommand: { requestType: 2, - argsArray: redis_request.Command.ArgsArray.create({ - args: ["bar1", "bar2"], + argsArray: command_request.Command.ArgsArray.create({ + args: convertStringArrayToBuffer(["bar1", "bar2"]), }), }, }; @@ -80,98 +84,97 @@ describe("RedisClient", () => { callbackIdx: 3, singleCommand: { requestType: 4, - argsArray: redis_request.Command.ArgsArray.create({ - args: ["bar3", "bar4"], + argsArray: command_request.Command.ArgsArray.create({ + args: convertStringArrayToBuffer(["bar3", "bar4"]), }), }, }; - redis_request.RedisRequest.encodeDelimited(request, writer); - redis_request.RedisRequest.encodeDelimited(request2, writer); + command_request.CommandRequest.encodeDelimited(request, writer); + command_request.CommandRequest.encodeDelimited(request2, writer); const buffer = writer.finish(); const reader = new BufferReader(buffer); - const dec_msg1 = redis_request.RedisRequest.decodeDelimited(reader); + const dec_msg1 = command_request.CommandRequest.decodeDelimited(reader); expect(dec_msg1.callbackIdx).toEqual(1); expect(dec_msg1.singleCommand?.requestType).toEqual(2); - expect(dec_msg1.singleCommand?.argsArray?.args).toEqual([ - "bar1", - "bar2", - ]); + expect(dec_msg1.singleCommand?.argsArray?.args).toEqual( + convertStringArrayToBuffer(["bar1", "bar2"]), + ); - const dec_msg2 = redis_request.RedisRequest.decodeDelimited(reader); + const dec_msg2 = command_request.CommandRequest.decodeDelimited(reader); expect(dec_msg2.callbackIdx).toEqual(3); expect(dec_msg2.singleCommand?.requestType).toEqual(4); - expect(dec_msg2.singleCommand?.argsArray?.args).toEqual([ - "bar3", - "bar4", - ]); + expect(dec_msg2.singleCommand?.argsArray?.args).toEqual( + convertStringArrayToBuffer(["bar3", "bar4"]), + ); }); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "info without parameters", async (protocol) => { - const client = await RedisClient.createClient( - getOptions(port, protocol), + client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const result = await client.info(); - expect(result).toEqual(expect.stringContaining("# Server")); - expect(result).toEqual(expect.stringContaining("# Replication")); - expect(result).toEqual( + expect(intoString(result)).toEqual( + expect.stringContaining("# Server"), + ); + expect(intoString(result)).toEqual( + expect.stringContaining("# Replication"), + ); + expect(intoString(result)).toEqual( expect.not.stringContaining("# Latencystats"), ); - client.close(); }, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "simple select test", async (protocol) => { - const client = await RedisClient.createClient( - getOptions(port, protocol), + client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); let selectResult = await client.select(0); - expect(selectResult).toEqual("OK"); + checkSimple(selectResult).toEqual("OK"); const key = uuidv4(); const value = uuidv4(); const result = await client.set(key, value); - expect(result).toEqual("OK"); + checkSimple(result).toEqual("OK"); selectResult = await client.select(1); - expect(selectResult).toEqual("OK"); + checkSimple(selectResult).toEqual("OK"); expect(await client.get(key)).toEqual(null); selectResult = await client.select(0); - expect(selectResult).toEqual("OK"); - expect(await client.get(key)).toEqual(value); - client.close(); + checkSimple(selectResult).toEqual("OK"); + checkSimple(await client.get(key)).toEqual(value); }, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `can send transactions_%p`, async (protocol) => { - const client = await RedisClient.createClient( - getOptions(port, protocol), + client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new Transaction(); const expectedRes = await transactionTest(transaction); transaction.select(0); const result = await client.exec(transaction); expectedRes.push("OK"); - expect(result).toEqual(expectedRes); - client.close(); + expect(intoString(result)).toEqual(intoString(expectedRes)); }, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "can return null on WATCH transaction failures", async (protocol) => { - const client1 = await RedisClient.createClient( - getOptions(port, protocol), + const client1 = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); - const client2 = await RedisClient.createClient( - getOptions(port, protocol), + const client2 = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new Transaction(); transaction.get("key"); @@ -189,21 +192,268 @@ describe("RedisClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object freq transaction test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new Transaction(); + transaction.configSet({ + [maxmemoryPolicyKey]: "allkeys-lfu", + }); + transaction.set(key, "foo"); + transaction.objectFreq(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + expect(response[0]).toEqual("OK"); + expect(response[1]).toEqual("OK"); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object idletime transaction test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new Transaction(); + transaction.configSet({ + // OBJECT IDLETIME requires a non-LFU maxmemory-policy + [maxmemoryPolicyKey]: "allkeys-random", + }); + transaction.set(key, "foo"); + transaction.objectIdletime(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + // transaction.configSet({[maxmemoryPolicyKey]: "allkeys-random"}); + expect(response[0]).toEqual("OK"); + // transaction.set(key, "foo"); + expect(response[1]).toEqual("OK"); + // transaction.objectIdletime(key); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object refcount transaction test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const transaction = new Transaction(); + transaction.set(key, "foo"); + transaction.objectRefcount(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(2); + expect(response[0]).toEqual("OK"); // transaction.set(key, "foo"); + expect(response[1]).toBeGreaterThanOrEqual(1); // transaction.objectRefcount(key); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "lolwut test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const result = await client.lolwut(); + expect(intoString(result)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result2 = await client.lolwut({ parameters: [] }); + expect(intoString(result2)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result3 = await client.lolwut({ parameters: [50, 20] }); + expect(intoString(result3)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result4 = await client.lolwut({ version: 6 }); + expect(intoString(result4)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result5 = await client.lolwut({ + version: 5, + parameters: [30, 4, 4], + }); + expect(intoString(result5)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // transaction tests + const transaction = new Transaction(); + transaction.lolwut(); + transaction.lolwut({ version: 5 }); + transaction.lolwut({ parameters: [1, 2] }); + transaction.lolwut({ version: 6, parameters: [42] }); + const results = await client.exec(transaction); + + if (results) { + for (const element of results) { + expect(intoString(element)).toEqual( + expect.stringContaining("Redis ver. "), + ); + } + } else { + throw new Error("Invalid LOLWUT transaction test results."); + } + + client.close(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP3])("simple pubsub test", async (protocol) => { + const pattern = "*"; + const channel = "test-channel"; + const config: GlideClientConfiguration = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const channelsAndPatterns: Partial< + Record> + > = { + [GlideClientConfiguration.PubSubChannelModes.Exact]: new Set([ + channel, + ]), + [GlideClientConfiguration.PubSubChannelModes.Pattern]: new Set([ + pattern, + ]), + }; + config.pubsubSubscriptions = { + channelsAndPatterns: channelsAndPatterns, + }; + client = await GlideClient.createClient(config); + const clientTry = await GlideClient.createClient(config); + const context: PubSubMsg[] = []; + + function new_message(msg: PubSubMsg, context: PubSubMsg[]): void { + context.push(msg); + } + + const clientCallback = await GlideClient.createClient({ + addresses: config.addresses, + pubsubSubscriptions: { + channelsAndPatterns: channelsAndPatterns, + callback: new_message, + context: context, + }, + }); + const message = uuidv4(); + const asyncMessages: PubSubMsg[] = []; + const tryMessages: (PubSubMsg | null)[] = []; + + await client.publish(message, "test-channel"); + const sleep = new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep; + + for (let i = 0; i < 2; i++) { + asyncMessages.push(await client.getPubSubMessage()); + tryMessages.push(clientTry.tryGetPubSubMessage()); + } + + expect(clientTry.tryGetPubSubMessage()).toBeNull(); + expect(asyncMessages.length).toBe(2); + expect(tryMessages.length).toBe(2); + expect(context.length).toBe(2); + + // assert all api flavors produced the same messages + expect(asyncMessages).toEqual(tryMessages); + expect(asyncMessages).toEqual(context); + + let patternCount = 0; + + for (let i = 0; i < 2; i++) { + const pubsubMsg = asyncMessages[i]; + expect(pubsubMsg.channel.toString()).toBe(channel); + expect(pubsubMsg.message.toString()).toBe(message); + + if (pubsubMsg.pattern) { + patternCount++; + expect(pubsubMsg.pattern.toString()).toBe(pattern); + } + } + + expect(patternCount).toBe(1); + client.close(); + clientTry.close(); + clientCallback.close(); + }); + runBaseTests({ init: async (protocol, clientName?) => { - const options = getOptions(port, protocol); + const options = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); options.protocol = protocol; options.clientName = clientName; testsFailed += 1; - const client = await RedisClient.createClient(options); + client = await GlideClient.createClient(options); return { client, context: { client } }; }, close: (context: Context, testSucceeded: boolean) => { if (testSucceeded) { testsFailed -= 1; } - - context.client.close(); }, timeout: TIMEOUT, }); diff --git a/node/tests/RedisClientInternals.test.ts b/node/tests/RedisClientInternals.test.ts index 9842a4fffb..888b47c374 100644 --- a/node/tests/RedisClientInternals.test.ts +++ b/node/tests/RedisClientInternals.test.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { beforeAll, describe, expect, it } from "@jest/globals"; @@ -22,23 +22,23 @@ import { BaseClientConfiguration, ClosingError, ClusterClientConfiguration, + GlideClient, + GlideClientConfiguration, + GlideClusterClient, InfoOptions, Logger, - RedisClient, - RedisClientConfiguration, - RedisClusterClient, RequestError, ReturnType, SlotKeyTypes, Transaction, } from ".."; import { + command_request, connection_request, - redis_request, response, } from "../src/ProtobufMessage"; - -const { RequestType, RedisRequest } = redis_request; +import { convertStringArrayToBuffer, intoString } from "./TestUtilities"; +const { RequestType, CommandRequest } = command_request; beforeAll(() => { Logger.init("info"); @@ -124,11 +124,11 @@ function sendResponse( function getConnectionAndSocket( checkRequest?: (request: connection_request.ConnectionRequest) => boolean, - connectionOptions?: ClusterClientConfiguration | RedisClientConfiguration, + connectionOptions?: ClusterClientConfiguration | GlideClientConfiguration, isCluster?: boolean, ): Promise<{ socket: net.Socket; - connection: RedisClient | RedisClusterClient; + connection: GlideClient | GlideClusterClient; server: net.Server; }> { return new Promise((resolve, reject) => { @@ -136,7 +136,7 @@ function getConnectionAndSocket( path.join(os.tmpdir(), `socket_listener`), ); const socketName = path.join(temporaryFolder, "read"); - let connectionPromise: Promise; // eslint-disable-line prefer-const + let connectionPromise: Promise; // eslint-disable-line prefer-const const server = net .createServer(async (socket) => { socket.once("data", (data) => { @@ -174,8 +174,8 @@ function getConnectionAndSocket( addresses: [{ host: "foo" }], }; const connection = isCluster - ? await RedisClusterClient.__createClient(options, socket) - : await RedisClient.__createClient(options, socket); + ? await GlideClusterClient.__createClient(options, socket) + : await GlideClient.__createClient(options, socket); resolve(connection); }); @@ -184,7 +184,7 @@ function getConnectionAndSocket( } function closeTestResources( - connection: RedisClient | RedisClusterClient, + connection: GlideClient | GlideClusterClient, server: net.Server, socket: net.Socket, ) { @@ -195,7 +195,7 @@ function closeTestResources( async function testWithResources( testFunction: ( - connection: RedisClient | RedisClusterClient, + connection: GlideClient | GlideClusterClient, socket: net.Socket, ) => Promise, connectionOptions?: BaseClientConfiguration, @@ -212,7 +212,7 @@ async function testWithResources( async function testWithClusterResources( testFunction: ( - connection: RedisClusterClient, + connection: GlideClusterClient, socket: net.Socket, ) => Promise, connectionOptions?: BaseClientConfiguration, @@ -224,7 +224,7 @@ async function testWithClusterResources( ); try { - if (connection instanceof RedisClusterClient) { + if (connection instanceof GlideClusterClient) { await testFunction(connection, socket); } else { throw new Error("Not cluster connection"); @@ -235,15 +235,16 @@ async function testWithClusterResources( } async function testSentValueMatches(config: { - sendRequest: (client: RedisClient | RedisClusterClient) => Promise; - expectedRequestType: redis_request.RequestType | null | undefined; + sendRequest: (client: GlideClient | GlideClusterClient) => Promise; + expectedRequestType: command_request.RequestType | null | undefined; expectedValue: unknown; }) { let counter = 0; await testWithResources(async (connection, socket) => { socket.on("data", (data) => { const reader = Reader.create(data); - const request = redis_request.RedisRequest.decodeDelimited(reader); + const request = + command_request.CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( config.expectedRequestType, ); @@ -288,9 +289,9 @@ describe("SocketConnectionInternals", () => { await testWithResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); expect( request.singleCommand?.argsArray?.args?.length, @@ -306,7 +307,7 @@ describe("SocketConnectionInternals", () => { ); }); const result = await connection.get("foo"); - expect(result).toEqual(expected); + expect(intoString(result)).toEqual(intoString(expected)); }); }; @@ -342,9 +343,9 @@ describe("SocketConnectionInternals", () => { await testWithResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); expect(request.singleCommand?.argsArray?.args?.length).toEqual( 1, @@ -361,11 +362,11 @@ describe("SocketConnectionInternals", () => { await testWithClusterResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect( request.transaction?.commands?.at(0)?.requestType, - ).toEqual(RequestType.SetString); + ).toEqual(RequestType.Set); expect( request.transaction?.commands?.at(0)?.argsArray?.args ?.length, @@ -390,7 +391,7 @@ describe("SocketConnectionInternals", () => { await testWithClusterResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect( request.transaction?.commands?.at(0)?.requestType, @@ -400,7 +401,7 @@ describe("SocketConnectionInternals", () => { ?.length, ).toEqual(1); expect(request.route?.simpleRoutes).toEqual( - redis_request.SimpleRoutes.Random, + command_request.SimpleRoutes.Random, ); sendResponse(socket, ResponseType.Value, request.callbackIdx, { @@ -410,7 +411,9 @@ describe("SocketConnectionInternals", () => { const transaction = new Transaction(); transaction.info([InfoOptions.Server]); const result = await connection.exec(transaction, "randomNode"); - expect(result).toEqual(expect.stringContaining("# Server")); + expect(intoString(result)).toEqual( + expect.stringContaining("# Server"), + ); }); }); @@ -418,9 +421,9 @@ describe("SocketConnectionInternals", () => { await testWithResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.SetString, + RequestType.Set, ); expect(request.singleCommand?.argsArray?.args?.length).toEqual( 2, @@ -438,9 +441,9 @@ describe("SocketConnectionInternals", () => { const error = "check"; socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); expect(request.singleCommand?.argsArray?.args?.length).toEqual( 1, @@ -463,9 +466,9 @@ describe("SocketConnectionInternals", () => { const error = "check"; socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); expect(request.singleCommand?.argsArray?.args?.length).toEqual( 1, @@ -491,9 +494,9 @@ describe("SocketConnectionInternals", () => { socket.once("data", (data) => { const reader = Reader.create(data); const request = - redis_request.RedisRequest.decodeDelimited(reader); + command_request.CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); sendResponse( socket, @@ -514,9 +517,9 @@ describe("SocketConnectionInternals", () => { await testWithResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); - const request = RedisRequest.decodeDelimited(reader); + const request = CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.SetString, + RequestType.Set, ); const args = request.singleCommand?.argsArray?.args; @@ -524,12 +527,13 @@ describe("SocketConnectionInternals", () => { throw new Error("no args"); } - expect(args.length).toEqual(5); - expect(args[0]).toEqual("foo"); - expect(args[1]).toEqual("bar"); - expect(args[2]).toEqual("XX"); - expect(args[3]).toEqual("GET"); - expect(args[4]).toEqual("EX 10"); + expect(args.length).toEqual(6); + expect(args[0]).toEqual(Buffer.from("foo")); + expect(args[1]).toEqual(Buffer.from("bar")); + expect(args[2]).toEqual(Buffer.from("XX")); + expect(args[3]).toEqual(Buffer.from("GET")); + expect(args[4]).toEqual(Buffer.from("EX")); + expect(args[5]).toEqual(Buffer.from("10")); sendResponse(socket, ResponseType.OK, request.callbackIdx); }); const request1 = connection.set("foo", "bar", { @@ -538,7 +542,7 @@ describe("SocketConnectionInternals", () => { expiry: { type: "seconds", count: 10 }, }); - await expect(await request1).toMatch("OK"); + expect(await request1).toMatch("OK"); }); }); @@ -547,9 +551,9 @@ describe("SocketConnectionInternals", () => { socket.once("data", (data) => { const reader = Reader.create(data); const request = - redis_request.RedisRequest.decodeDelimited(reader); + command_request.CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); expect(request.singleCommand?.argsVecPointer).not.toBeNull(); expect(request.singleCommand?.argsArray).toBeNull(); @@ -568,9 +572,9 @@ describe("SocketConnectionInternals", () => { socket.once("data", (data) => { const reader = Reader.create(data); const request = - redis_request.RedisRequest.decodeDelimited(reader); + command_request.CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( - RequestType.GetString, + RequestType.Get, ); expect(request.singleCommand?.argsArray).not.toBeNull(); expect(request.singleCommand?.argsVecPointer).toBeNull(); @@ -661,24 +665,34 @@ describe("SocketConnectionInternals", () => { socket.on("data", (data) => { const reader = Reader.create(data); const request = - redis_request.RedisRequest.decodeDelimited(reader); + command_request.CommandRequest.decodeDelimited(reader); expect(request.singleCommand?.requestType).toEqual( RequestType.CustomCommand, ); - if (request.singleCommand?.argsArray?.args?.at(0) === "SET") { + if ( + request + .singleCommand!.argsArray!.args!.at(0)! + .toString() === "SET" + ) { expect(request.route?.simpleRoutes).toEqual( - redis_request.SimpleRoutes.AllPrimaries, + command_request.SimpleRoutes.AllPrimaries, ); } else if ( - request.singleCommand?.argsArray?.args?.at(0) === "GET" + request + .singleCommand!.argsArray!.args!.at(0)! + .toString() === "GET" ) { expect(request.route?.slotKeyRoute).toEqual({ - slotType: redis_request.SlotTypes.Replica, + slotType: command_request.SlotTypes.Replica, slotKey: "foo", }); } else { - throw new Error("unexpected command"); + throw new Error( + "unexpected command: [" + + request.singleCommand!.argsArray!.args!.at(0) + + "]", + ); } sendResponse(socket, ResponseType.Null, request.callbackIdx); @@ -705,7 +719,14 @@ describe("SocketConnectionInternals", () => { ["b", "2"], ]), expectedRequestType: RequestType.XAdd, - expectedValue: ["foo", "*", "a", "1", "b", "2"], + expectedValue: convertStringArrayToBuffer([ + "foo", + "*", + "a", + "1", + "b", + "2", + ]), }); }); @@ -717,7 +738,12 @@ describe("SocketConnectionInternals", () => { makeStream: true, }), expectedRequestType: RequestType.XAdd, - expectedValue: ["bar", "YOLO", "a", "1"], + expectedValue: convertStringArrayToBuffer([ + "bar", + "YOLO", + "a", + "1", + ]), }); }); @@ -732,7 +758,15 @@ describe("SocketConnectionInternals", () => { }, }), expectedRequestType: RequestType.XAdd, - expectedValue: ["baz", "MAXLEN", "=", "1000", "*", "c", "3"], + expectedValue: convertStringArrayToBuffer([ + "baz", + "MAXLEN", + "=", + "1000", + "*", + "c", + "3", + ]), }); }); @@ -749,7 +783,7 @@ describe("SocketConnectionInternals", () => { }, }), expectedRequestType: RequestType.XAdd, - expectedValue: [ + expectedValue: convertStringArrayToBuffer([ "foobar", "NOMKSTREAM", "MINID", @@ -760,7 +794,7 @@ describe("SocketConnectionInternals", () => { "*", "d", "4", - ], + ]), }); }); @@ -773,7 +807,12 @@ describe("SocketConnectionInternals", () => { exact: true, }), expectedRequestType: RequestType.XTrim, - expectedValue: ["foo", "MAXLEN", "=", "1000"], + expectedValue: convertStringArrayToBuffer([ + "foo", + "MAXLEN", + "=", + "1000", + ]), }); }); @@ -787,7 +826,14 @@ describe("SocketConnectionInternals", () => { limit: 1000, }), expectedRequestType: RequestType.XTrim, - expectedValue: ["bar", "MINID", "~", "foo", "LIMIT", "1000"], + expectedValue: convertStringArrayToBuffer([ + "bar", + "MINID", + "~", + "foo", + "LIMIT", + "1000", + ]), }); }); @@ -799,7 +845,13 @@ describe("SocketConnectionInternals", () => { foobar: "baz", }), expectedRequestType: RequestType.XRead, - expectedValue: ["STREAMS", "foo", "foobar", "bar", "baz"], + expectedValue: convertStringArrayToBuffer([ + "STREAMS", + "foo", + "foobar", + "bar", + "baz", + ]), }); }); @@ -813,7 +865,13 @@ describe("SocketConnectionInternals", () => { }, ), expectedRequestType: RequestType.XRead, - expectedValue: ["BLOCK", "100", "STREAMS", "foo", "bar"], + expectedValue: convertStringArrayToBuffer([ + "BLOCK", + "100", + "STREAMS", + "foo", + "bar", + ]), }); }); @@ -827,7 +885,13 @@ describe("SocketConnectionInternals", () => { }, ), expectedRequestType: RequestType.XRead, - expectedValue: ["COUNT", "2", "STREAMS", "bar", "baz"], + expectedValue: convertStringArrayToBuffer([ + "COUNT", + "2", + "STREAMS", + "bar", + "baz", + ]), }); }); }); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index d30e809b2d..2b656aea45 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { @@ -11,36 +11,49 @@ import { it, } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; + import { - BaseClientConfiguration, + ClusterClientConfiguration, ClusterTransaction, + GlideClusterClient, InfoOptions, ProtocolVersion, - RedisClusterClient, } from ".."; -import { runBaseTests } from "./SharedTests"; +import { RedisCluster } from "../../utils/TestUtils.js"; +import { checkIfServerVersionLessThan, runBaseTests } from "./SharedTests"; import { - RedisCluster, - flushallOnPort, + flushAndCloseClient, + getClientConfigurationOption, getFirstResult, + intoArray, + intoString, + parseCommandLineArgs, + parseEndpoints, transactionTest, } from "./TestUtilities"; - type Context = { - client: RedisClusterClient; + client: GlideClusterClient; }; -const TIMEOUT = 10000; +const TIMEOUT = 50000; -describe("RedisClusterClient", () => { +describe("GlideClusterClient", () => { let testsFailed = 0; let cluster: RedisCluster; + let client: GlideClusterClient; beforeAll(async () => { - cluster = await RedisCluster.createCluster(true, 3, 0); + const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; + // Connect to cluster or create a new one based on the parsed addresses + cluster = clusterAddresses + ? RedisCluster.initFromExistingCluster( + parseEndpoints(clusterAddresses), + ) + : // setting replicaCount to 1 to facilitate tests routed to replicas + await RedisCluster.createCluster(true, 3, 1); }, 20000); afterEach(async () => { - await Promise.all(cluster.ports().map((port) => flushallOnPort(port))); + await flushAndCloseClient(true, cluster.getAddresses(), client); }); afterAll(async () => { @@ -49,26 +62,16 @@ describe("RedisClusterClient", () => { } }); - const getOptions = ( - ports: number[], - protocol: ProtocolVersion, - ): BaseClientConfiguration => { - return { - addresses: ports.map((port) => ({ - host: "localhost", - port, - })), - protocol, - }; - }; - runBaseTests({ init: async (protocol, clientName?) => { - const options = getOptions(cluster.ports(), protocol); + const options = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); options.protocol = protocol; options.clientName = clientName; testsFailed += 1; - const client = await RedisClusterClient.createClient(options); + client = await GlideClusterClient.createClient(options); return { context: { client, @@ -80,8 +83,6 @@ describe("RedisClusterClient", () => { if (testSucceeded) { testsFailed -= 1; } - - context.client.close(); }, timeout: TIMEOUT, }); @@ -89,31 +90,26 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `info with server and replication_%p`, async (protocol) => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const info_server = getFirstResult( await client.info([InfoOptions.Server]), ); - expect(info_server).toEqual(expect.stringContaining("# Server")); - - const result = (await client.info([ - InfoOptions.Replication, - ])) as Record; - const clusterNodes = await client.customCommand([ - "CLUSTER", - "NODES", - ]); - expect( - (clusterNodes as string)?.split("master").length - 1, - ).toEqual(Object.keys(result).length); - Object.values(result).every((item) => { - expect(item).toEqual(expect.stringContaining("# Replication")); - expect(item).toEqual( - expect.not.stringContaining("# Errorstats"), - ); - }); - client.close(); + expect(intoString(info_server)).toEqual( + expect.stringContaining("# Server"), + ); + + const infoReplicationValues = Object.values( + await client.info([InfoOptions.Replication]), + ); + + const replicationInfo = intoArray(infoReplicationValues); + + for (const item of replicationInfo) { + expect(item).toContain("role:master"); + expect(item).toContain("# Replication"); + } }, TIMEOUT, ); @@ -121,17 +117,19 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `info with server and randomNode route_%p`, async (protocol) => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const result = await client.info( [InfoOptions.Server], "randomNode", ); - expect(typeof result).toEqual("string"); - expect(result).toEqual(expect.stringContaining("# Server")); - expect(result).toEqual(expect.not.stringContaining("# Errorstats")); - client.close(); + expect(intoString(result)).toEqual( + expect.stringContaining("# Server"), + ); + expect(intoString(result)).toEqual( + expect.not.stringContaining("# Errorstats"), + ); }, TIMEOUT, ); @@ -149,14 +147,16 @@ describe("RedisClusterClient", () => { ); }; - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const result = cleanResult( - (await client.customCommand( - ["cluster", "nodes"], - "randomNode", - )) as string, + intoString( + await client.customCommand( + ["cluster", "nodes"], + "randomNode", + ), + ), ); // check that routing without explicit port works @@ -167,10 +167,12 @@ describe("RedisClusterClient", () => { } const secondResult = cleanResult( - (await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host, - })) as string, + intoString( + await client.customCommand(["cluster", "nodes"], { + type: "routeByAddress", + host, + }), + ), ); expect(result).toEqual(secondResult); @@ -179,16 +181,16 @@ describe("RedisClusterClient", () => { // check that routing with explicit port works const thirdResult = cleanResult( - (await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host: host2, - port: Number(port), - })) as string, + intoString( + await client.customCommand(["cluster", "nodes"], { + type: "routeByAddress", + host: host2, + port: Number(port), + }), + ), ); expect(result).toEqual(thirdResult); - - client.close(); }, TIMEOUT, ); @@ -196,8 +198,8 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `fail routing by address if no port is provided_%p`, async (protocol) => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); expect(() => client.info(undefined, { @@ -205,7 +207,6 @@ describe("RedisClusterClient", () => { host: "foo", }), ).toThrowError(); - client.close(); }, TIMEOUT, ); @@ -213,15 +214,16 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set transactions test_%p`, async (protocol) => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new ClusterTransaction(); transaction.configSet({ timeout: "1000" }); transaction.configGet(["timeout"]); const result = await client.exec(transaction); - expect(result).toEqual(["OK", { timeout: "1000" }]); - client.close(); + expect(intoString(result)).toEqual( + intoString(["OK", { timeout: "1000" }]), + ); }, TIMEOUT, ); @@ -229,14 +231,13 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `can send transactions_%p`, async (protocol) => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new ClusterTransaction(); const expectedRes = await transactionTest(transaction); const result = await client.exec(transaction); - expect(result).toEqual(expectedRes); - client.close(); + expect(intoString(result)).toEqual(intoString(expectedRes)); }, TIMEOUT, ); @@ -244,11 +245,11 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `can return null on WATCH transaction failures_%p`, async (protocol) => { - const client1 = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + const client1 = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); - const client2 = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + const client2 = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const transaction = new ClusterTransaction(); transaction.get("key"); @@ -270,18 +271,305 @@ describe("RedisClusterClient", () => { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `echo with all nodes routing_%p`, async (protocol) => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports(), protocol), + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), ); const message = uuidv4(); const echoDict = await client.echo(message, "allNodes"); expect(typeof echoDict).toBe("object"); - expect(Object.values(echoDict)).toEqual( - expect.arrayContaining([message]), + expect(intoArray(echoDict)).toEqual( + expect.arrayContaining(intoArray([message])), ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `check that multi key command returns a cross slot error`, + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const versionLessThan7 = + await checkIfServerVersionLessThan("7.0.0"); + + const promises: Promise[] = [ + client.blpop(["abc", "zxy", "lkn"], 0.1), + client.rename("abc", "zxy"), + client.brpop(["abc", "zxy", "lkn"], 0.1), + client.smove("abc", "zxy", "value"), + client.renamenx("abc", "zxy"), + client.sinter(["abc", "zxy", "lkn"]), + client.sintercard(["abc", "zxy", "lkn"]), + client.sinterstore("abc", ["zxy", "lkn"]), + client.zinterstore("abc", ["zxy", "lkn"]), + client.sunionstore("abc", ["zxy", "lkn"]), + client.sunion(["abc", "zxy", "lkn"]), + client.pfcount(["abc", "zxy", "lkn"]), + client.sdiff(["abc", "zxy", "lkn"]), + client.sdiffstore("abc", ["zxy", "lkn"]), + // TODO all rest multi-key commands except ones tested below + ]; + + if (!versionLessThan7) { + promises.push(client.zintercard(["abc", "zxy", "lkn"])); + } + + for (const promise of promises) { + try { + await promise; + } catch (e) { + expect((e as Error).message.toLowerCase()).toContain( + "crossslot", + ); + } + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `check that multi key command routed to multiple nodes`, + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + await client.exists(["abc", "zxy", "lkn"]); + await client.unlink(["abc", "zxy", "lkn"]); + await client.del(["abc", "zxy", "lkn"]); + await client.mget(["abc", "zxy", "lkn"]); + await client.mset({ abc: "1", zxy: "2", lkn: "3" }); + // TODO touch + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object freq transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new ClusterTransaction(); + transaction.configSet({ + [maxmemoryPolicyKey]: "allkeys-lfu", + }); + transaction.set(key, "foo"); + transaction.objectFreq(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + expect(response[0]).toEqual("OK"); + expect(response[1]).toEqual("OK"); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object idletime transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new ClusterTransaction(); + transaction.configSet({ + // OBJECT IDLETIME requires a non-LFU maxmemory-policy + [maxmemoryPolicyKey]: "allkeys-random", + }); + transaction.set(key, "foo"); + transaction.objectIdletime(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + // transaction.configSet({[maxmemoryPolicyKey]: "allkeys-random"}); + expect(response[0]).toEqual("OK"); + // transaction.set(key, "foo"); + expect(response[1]).toEqual("OK"); + // transaction.objectIdletime(key); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object refcount transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const transaction = new ClusterTransaction(); + transaction.set(key, "foo"); + transaction.objectRefcount(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(2); + expect(response[0]).toEqual("OK"); // transaction.set(key, "foo"); + expect(response[1]).toBeGreaterThanOrEqual(1); // transaction.objectRefcount(key); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lolwut test_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + // test with multi-node route + const result1 = await client.lolwut({}, "allNodes"); + expect(intoString(result1)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result2 = await client.lolwut( + { version: 2, parameters: [10, 20] }, + "allNodes", + ); + expect(intoString(result2)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // test with single-node route + const result3 = await client.lolwut({}, "randomNode"); + expect(intoString(result3)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result4 = await client.lolwut( + { version: 2, parameters: [10, 20] }, + "randomNode", + ); + expect(intoString(result4)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // transaction tests + const transaction = new ClusterTransaction(); + transaction.lolwut(); + transaction.lolwut({ version: 5 }); + transaction.lolwut({ parameters: [1, 2] }); + transaction.lolwut({ version: 6, parameters: [42] }); + const results = await client.exec(transaction); + + if (results) { + for (const element of results) { + expect(intoString(element)).toEqual( + expect.stringContaining("Redis ver. "), + ); + } + } else { + throw new Error("Invalid LOLWUT transaction test results."); + } + client.close(); }, TIMEOUT, ); + + it.each([ + [true, ProtocolVersion.RESP3], + [false, ProtocolVersion.RESP3], + ])("simple pubsub test", async (sharded, protocol) => { + if (sharded && (await checkIfServerVersionLessThan("7.2.0"))) { + return; + } + + const channel = "test-channel"; + const shardedChannel = "test-channel-sharded"; + const channelsAndPatterns: Partial< + Record> + > = { + [ClusterClientConfiguration.PubSubChannelModes.Exact]: new Set([ + channel, + ]), + }; + + if (sharded) { + channelsAndPatterns[ + ClusterClientConfiguration.PubSubChannelModes.Sharded + ] = new Set([shardedChannel]); + } + + const config: ClusterClientConfiguration = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + config.pubsubSubscriptions = { + channelsAndPatterns: channelsAndPatterns, + }; + client = await GlideClusterClient.createClient(config); + const message = uuidv4(); + + await client.publish(message, channel); + const sleep = new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep; + + let pubsubMsg = await client.getPubSubMessage(); + expect(pubsubMsg.channel.toString()).toBe(channel); + expect(pubsubMsg.message.toString()).toBe(message); + expect(pubsubMsg.pattern).toBeNull(); + + if (sharded) { + await client.publish(message, shardedChannel, true); + await sleep; + pubsubMsg = await client.getPubSubMessage(); + console.log(pubsubMsg); + expect(pubsubMsg.channel.toString()).toBe(shardedChannel); + expect(pubsubMsg.message.toString()).toBe(message); + expect(pubsubMsg.pattern).toBeNull(); + } + + client.close(); + }); }); diff --git a/node/tests/RedisModules.test.ts b/node/tests/RedisModules.test.ts deleted file mode 100644 index e321d1d9ea..0000000000 --- a/node/tests/RedisModules.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -import { - afterAll, - afterEach, - beforeAll, - describe, - expect, - it, -} from "@jest/globals"; -import { - BaseClientConfiguration, - InfoOptions, - RedisClusterClient, - parseInfoResponse, -} from "../"; -import { runBaseTests } from "./SharedTests"; -import { RedisCluster, flushallOnPort, getFirstResult } from "./TestUtilities"; - -type Context = { - client: RedisClusterClient; -}; - -const TIMEOUT = 10000; - -describe("RedisModules", () => { - let testsFailed = 0; - let cluster: RedisCluster; - beforeAll(async () => { - const args = process.argv.slice(2); - const loadModuleArgs = args.filter((arg) => - arg.startsWith("--load-module="), - ); - const loadModuleValues = loadModuleArgs.map((arg) => arg.split("=")[1]); - cluster = await RedisCluster.createCluster( - true, - 3, - 0, - loadModuleValues, - ); - }, 20000); - - afterEach(async () => { - await Promise.all(cluster.ports().map((port) => flushallOnPort(port))); - }); - - afterAll(async () => { - if (testsFailed === 0) { - await cluster.close(); - } - }); - - const getOptions = (ports: number[]): BaseClientConfiguration => { - return { - addresses: ports.map((port) => ({ - host: "localhost", - port, - })), - }; - }; - - runBaseTests({ - init: async (protocol, clientName) => { - const options = getOptions(cluster.ports()); - options.protocol = protocol; - options.clientName = clientName; - testsFailed += 1; - const client = await RedisClusterClient.createClient(options); - return { - context: { - client, - }, - client, - }; - }, - close: (context: Context, testSucceeded: boolean) => { - if (testSucceeded) { - testsFailed -= 1; - } - - context.client.close(); - }, - timeout: TIMEOUT, - }); - - it("simple json test", async () => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports()), - ); - const info = parseInfoResponse( - getFirstResult(await client.info([InfoOptions.Modules])).toString(), - )["module"]; - expect(info).toEqual(expect.stringContaining("ReJSON")); - client.close(); - }); -}); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index eedb43a015..470c60a77e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1,5 +1,5 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { expect, it } from "@jest/globals"; @@ -8,14 +8,27 @@ import { v4 as uuidv4 } from "uuid"; import { ClosingError, ExpireOptions, + FlushMode, + GlideClient, + GlideClusterClient, InfoOptions, + InsertPosition, ProtocolVersion, - RedisClient, - RedisClusterClient, + RequestError, Script, parseInfoResponse, } from "../"; -import { Client, GetAndSetRandomValue, getFirstResult } from "./TestUtilities"; +import { + Client, + GetAndSetRandomValue, + checkSimple, + compareMaps, + getFirstResult, + intoArray, + intoString, +} from "./TestUtilities"; +import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; +import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { @@ -50,7 +63,7 @@ export async function checkIfServerVersionLessThan( return versionToCompare < minVersion; } -export type BaseClient = RedisClient | RedisClusterClient; +export type BaseClient = GlideClient | GlideClusterClient; export function runBaseTests(config: { init: ( @@ -94,9 +107,8 @@ export function runBaseTests(config: { } const result = await client.customCommand(["CLIENT", "INFO"]); - - expect(result).toContain("lib-name=GlideJS"); - expect(result).toContain("lib-ver=unknown"); + expect(intoString(result)).toContain("lib-name=GlideJS"); + expect(intoString(result)).toContain("lib-ver=unknown"); }, protocol); }, config.timeout, @@ -151,7 +163,9 @@ export function runBaseTests(config: { async (protocol) => { await runTest( async (client: BaseClient) => { - expect(await client.clientGetName()).toBe("TEST_CLIENT"); + expect(intoString(await client.clientGetName())).toBe( + "TEST_CLIENT", + ); }, protocol, "TEST_CLIENT", @@ -160,63 +174,6 @@ export function runBaseTests(config: { config.timeout, ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `set with return of old value works_%p`, - async (protocol) => { - await runTest(async (client: BaseClient) => { - const key = uuidv4(); - // Adding random repetition, to prevent the inputs from always having the same alignment. - const value = uuidv4() + "0".repeat(Math.random() * 7); - - let result = await client.set(key, value); - expect(result).toEqual("OK"); - - result = await client.set(key, "", { - returnOldValue: true, - }); - expect(result).toEqual(value); - - result = await client.get(key); - expect(result).toEqual(""); - }, protocol); - }, - config.timeout, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `conditional set works_%p`, - async (protocol) => { - await runTest(async (client: BaseClient) => { - const key = uuidv4(); - // Adding random repetition, to prevent the inputs from always having the same alignment. - const value = uuidv4() + "0".repeat(Math.random() * 7); - let result = await client.set(key, value, { - conditionalSet: "onlyIfExists", - }); - expect(result).toEqual(null); - - result = await client.set(key, value, { - conditionalSet: "onlyIfDoesNotExist", - }); - expect(result).toEqual("OK"); - expect(await client.get(key)).toEqual(value); - - result = await client.set(key, "foobar", { - conditionalSet: "onlyIfDoesNotExist", - }); - expect(result).toEqual(null); - - result = await client.set(key, "foobar", { - conditionalSet: "onlyIfExists", - }); - expect(result).toEqual("OK"); - - expect(await client.get(key)).toEqual("foobar"); - }, protocol); - }, - config.timeout, - ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `custom command works_%p`, async (protocol) => { @@ -229,9 +186,9 @@ export function runBaseTests(config: { key, value, ]); - expect(setResult).toEqual("OK"); + checkSimple(setResult).toEqual("OK"); const result = await client.customCommand(["GET", key]); - expect(result).toEqual(value); + checkSimple(result).toEqual(value); }, protocol); }, config.timeout, @@ -252,20 +209,20 @@ export function runBaseTests(config: { key1, value1, ]); - expect(setResult1).toEqual("OK"); + checkSimple(setResult1).toEqual("OK"); const setResult2 = await client.customCommand([ "SET", key2, value2, ]); - expect(setResult2).toEqual("OK"); + checkSimple(setResult2).toEqual("OK"); const mget_result = await client.customCommand([ "MGET", key1, key2, key3, ]); - expect(mget_result).toEqual([value1, value2, null]); + checkSimple(mget_result).toEqual([value1, value2, null]); }, protocol); }, config.timeout, @@ -308,13 +265,15 @@ export function runBaseTests(config: { `test config rewrite_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const serverInfo = await client.info([InfoOptions.Server]); + const serverInfo = intoString( + await client.info([InfoOptions.Server]), + ); const conf_file = parseInfoResponse( getFirstResult(serverInfo).toString(), )["config_file"]; if (conf_file.length > 0) { - expect(await client.configRewrite()).toEqual("OK"); + checkSimple(await client.configRewrite()).toEqual("OK"); } else { try { /// We expect Redis to return an error since the test cluster doesn't use redis.conf file @@ -337,13 +296,17 @@ export function runBaseTests(config: { /// we execute set and info so the commandstats will show `cmdstat_set::calls` greater than 1 /// after the configResetStat call we initiate an info command and the the commandstats won't contain `cmdstat_set`. await client.set("foo", "bar"); - const OldResult = await client.info([InfoOptions.Commandstats]); - expect(JSON.stringify(OldResult)).toContain("cmdstat_set"); - - expect(await client.configResetStat()).toEqual("OK"); - - const result = await client.info([InfoOptions.Commandstats]); - expect(JSON.stringify(result)).not.toContain("cmdstat_set"); + const oldResult = await client.info([InfoOptions.Commandstats]); + const oldResultAsString = intoString(oldResult); + console.log(oldResult); + console.log(oldResultAsString); + expect(oldResultAsString).toContain("cmdstat_set"); + checkSimple(await client.configResetStat()).toEqual("OK"); + + const result = intoArray( + await client.info([InfoOptions.Commandstats]), + ); + expect(result).not.toContain("cmdstat_set"); }, protocol); }, config.timeout, @@ -362,8 +325,8 @@ export function runBaseTests(config: { [key2]: value, [key3]: value, }; - expect(await client.mset(keyValueList)).toEqual("OK"); - expect( + checkSimple(await client.mset(keyValueList)).toEqual("OK"); + checkSimple( await client.mget([key1, key2, "nonExistingKey", key3]), ).toEqual([value, value, null, value]); }, protocol); @@ -376,13 +339,13 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "10")).toEqual("OK"); + checkSimple(await client.set(key, "10")).toEqual("OK"); expect(await client.incr(key)).toEqual(11); - expect(await client.get(key)).toEqual("11"); - expect(await client.incrBy(key, 4)).toEqual(15); - expect(await client.get(key)).toEqual("15"); - expect(await client.incrByFloat(key, 1.5)).toEqual(16.5); - expect(await client.get(key)).toEqual("16.5"); + checkSimple(await client.get(key)).toEqual("11"); + checkSimple(await client.incrBy(key, 4)).toEqual(15); + checkSimple(await client.get(key)).toEqual("15"); + checkSimple(await client.incrByFloat(key, 1.5)).toEqual(16.5); + checkSimple(await client.get(key)).toEqual("16.5"); }, protocol); }, config.timeout, @@ -397,11 +360,11 @@ export function runBaseTests(config: { const key3 = uuidv4(); /// key1 and key2 does not exist, so it set to 0 before performing the operation. expect(await client.incr(key1)).toEqual(1); - expect(await client.get(key1)).toEqual("1"); + checkSimple(await client.get(key1)).toEqual("1"); expect(await client.incrBy(key2, 2)).toEqual(2); - expect(await client.get(key2)).toEqual("2"); + checkSimple(await client.get(key2)).toEqual("2"); expect(await client.incrByFloat(key3, -0.5)).toEqual(-0.5); - expect(await client.get(key3)).toEqual("-0.5"); + checkSimple(await client.get(key3)).toEqual("-0.5"); }, protocol); }, config.timeout, @@ -412,7 +375,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.incr(key)).toThrow(); @@ -446,8 +409,8 @@ export function runBaseTests(config: { `ping test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - expect(await client.ping()).toEqual("PONG"); - expect(await client.ping("Hello")).toEqual("Hello"); + checkSimple(await client.ping()).toEqual("PONG"); + checkSimple(await client.ping("Hello")).toEqual("Hello"); }, protocol); }, config.timeout, @@ -470,11 +433,11 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "10")).toEqual("OK"); + checkSimple(await client.set(key, "10")).toEqual("OK"); expect(await client.decr(key)).toEqual(9); - expect(await client.get(key)).toEqual("9"); + checkSimple(await client.get(key)).toEqual("9"); expect(await client.decrBy(key, 4)).toEqual(5); - expect(await client.get(key)).toEqual("5"); + checkSimple(await client.get(key)).toEqual("5"); }, protocol); }, config.timeout, @@ -489,10 +452,10 @@ export function runBaseTests(config: { /// key1 and key2 does not exist, so it set to 0 before performing the operation. expect(await client.get(key1)).toBeNull(); expect(await client.decr(key1)).toEqual(-1); - expect(await client.get(key1)).toEqual("-1"); + checkSimple(await client.get(key1)).toEqual("-1"); expect(await client.get(key2)).toBeNull(); expect(await client.decrBy(key2, 3)).toEqual(-3); - expect(await client.get(key2)).toEqual("-3"); + checkSimple(await client.get(key2)).toEqual("-3"); }, protocol); }, config.timeout, @@ -532,15 +495,15 @@ export function runBaseTests(config: { const prevTimeout = (await client.configGet([ "timeout", ])) as Record; - expect(await client.configSet({ timeout: "1000" })).toEqual( - "OK", - ); + checkSimple( + await client.configSet({ timeout: "1000" }), + ).toEqual("OK"); const currTimeout = (await client.configGet([ "timeout", ])) as Record; - expect(currTimeout).toEqual({ timeout: "1000" }); + checkSimple(currTimeout).toEqual({ timeout: "1000" }); /// Revert to the pervious configuration - expect( + checkSimple( await client.configSet({ timeout: prevTimeout["timeout"], }), @@ -563,8 +526,8 @@ export function runBaseTests(config: { [field2]: value, }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect(await client.hget(key, field1)).toEqual(value); - expect(await client.hget(key, field2)).toEqual(value); + checkSimple(await client.hget(key, field1)).toEqual(value); + checkSimple(await client.hget(key, field2)).toEqual(value); expect(await client.hget(key, "nonExistingField")).toEqual( null, ); @@ -612,7 +575,7 @@ export function runBaseTests(config: { [field2]: value, }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect( + checkSimple( await client.hmget(key, [ field1, "nonExistingField", @@ -664,10 +627,14 @@ export function runBaseTests(config: { [field2]: value, }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect(await client.hgetall(key)).toEqual({ - [field1]: value, - [field2]: value, - }); + + expect(intoString(await client.hgetall(key))).toEqual( + intoString({ + [field1]: value, + [field2]: value, + }), + ); + expect(await client.hgetall("nonExistingKey")).toEqual({}); }, protocol); }, @@ -788,9 +755,12 @@ export function runBaseTests(config: { }; expect(await client.hset(key1, fieldValueMap)).toEqual(2); - expect(await client.hvals(key1)).toEqual(["value1", "value2"]); + checkSimple(await client.hvals(key1)).toEqual([ + "value1", + "value2", + ]); expect(await client.hdel(key1, [field1])).toEqual(1); - expect(await client.hvals(key1)).toEqual(["value2"]); + checkSimple(await client.hvals(key1)).toEqual(["value2"]); expect(await client.hvals("nonExistingHash")).toEqual([]); }, protocol); }, @@ -809,9 +779,9 @@ export function runBaseTests(config: { expect(await client.hsetnx(key1, field, "newValue")).toEqual( false, ); - expect(await client.hget(key1, field)).toEqual("value"); + checkSimple(await client.hget(key1, field)).toEqual("value"); - expect(await client.set(key2, "value")).toEqual("OK"); + checkSimple(await client.set(key2, "value")).toEqual("OK"); await expect( client.hsetnx(key2, field, "value"), ).rejects.toThrow(); @@ -827,13 +797,13 @@ export function runBaseTests(config: { const key = uuidv4(); const valueList = ["value4", "value3", "value2", "value1"]; expect(await client.lpush(key, valueList)).toEqual(4); - expect(await client.lpop(key)).toEqual("value1"); - expect(await client.lrange(key, 0, -1)).toEqual([ + checkSimple(await client.lpop(key)).toEqual("value1"); + checkSimple(await client.lrange(key, 0, -1)).toEqual([ "value2", "value3", "value4", ]); - expect(await client.lpopCount(key, 2)).toEqual([ + checkSimple(await client.lpopCount(key, 2)).toEqual([ "value2", "value3", ]); @@ -851,7 +821,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.lpush(key, ["bar"])).toThrow(); @@ -881,6 +851,41 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lpushx list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const key3 = uuidv4(); + + expect(await client.lpush(key1, ["0"])).toEqual(1); + expect(await client.lpushx(key1, ["1", "2", "3"])).toEqual(4); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "3", + "2", + "1", + "0", + ]); + + expect(await client.lpushx(key2, ["1"])).toEqual(0); + checkSimple(await client.lrange(key2, 0, -1)).toEqual([]); + + // Key exists, but is not a list + checkSimple(await client.set(key3, "bar")); + await expect(client.lpushx(key3, ["_"])).rejects.toThrow( + RequestError, + ); + + // Empty element list + await expect(client.lpushx(key2, [])).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `llen with existing, non-existing key and key that holds a value that is not a list_%p`, async (protocol) => { @@ -893,7 +898,7 @@ export function runBaseTests(config: { expect(await client.llen("nonExistingKey")).toEqual(0); - expect(await client.set(key2, "foo")).toEqual("OK"); + checkSimple(await client.set(key2, "foo")).toEqual("OK"); try { expect(await client.llen(key2)).toThrow(); @@ -907,6 +912,60 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lset test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonExistingKey = uuidv4(); + const index = 0; + const oobIndex = 10; + const negativeIndex = -1; + const element = "zero"; + const lpushArgs = ["four", "three", "two", "one"]; + const expectedList = ["zero", "two", "three", "four"]; + const expectedList2 = ["zero", "two", "three", "zero"]; + + // key does not exist + await expect( + client.lset(nonExistingKey, index, element), + ).rejects.toThrow(RequestError); + + expect(await client.lpush(key, lpushArgs)).toEqual(4); + + // index out of range + await expect( + client.lset(key, oobIndex, element), + ).rejects.toThrow(RequestError); + + // assert lset result + checkSimple(await client.lset(key, index, element)).toEqual( + "OK", + ); + checkSimple(await client.lrange(key, 0, negativeIndex)).toEqual( + expectedList, + ); + + // assert lset with a negative index for the last element in the list + checkSimple( + await client.lset(key, negativeIndex, element), + ).toEqual("OK"); + checkSimple(await client.lrange(key, 0, negativeIndex)).toEqual( + expectedList2, + ); + + // assert lset against a non-list key + const nonListKey = "nonListKey"; + expect(await client.sadd(nonListKey, ["a"])).toEqual(1); + + await expect(client.lset(nonListKey, 0, "b")).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `ltrim with existing key and key that holds a value that is not a list_%p`, async (protocol) => { @@ -914,17 +973,17 @@ export function runBaseTests(config: { const key = uuidv4(); const valueList = ["value4", "value3", "value2", "value1"]; expect(await client.lpush(key, valueList)).toEqual(4); - expect(await client.ltrim(key, 0, 1)).toEqual("OK"); - expect(await client.lrange(key, 0, -1)).toEqual([ + checkSimple(await client.ltrim(key, 0, 1)).toEqual("OK"); + checkSimple(await client.lrange(key, 0, -1)).toEqual([ "value1", "value2", ]); /// `start` is greater than `end` so the key will be removed. - expect(await client.ltrim(key, 4, 2)).toEqual("OK"); + checkSimple(await client.ltrim(key, 4, 2)).toEqual("OK"); expect(await client.lrange(key, 0, -1)).toEqual([]); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.ltrim(key, 0, 1)).toThrow(); @@ -952,18 +1011,20 @@ export function runBaseTests(config: { ]; expect(await client.lpush(key, valueList)).toEqual(5); expect(await client.lrem(key, 2, "value1")).toEqual(2); - expect(await client.lrange(key, 0, -1)).toEqual([ + checkSimple(await client.lrange(key, 0, -1)).toEqual([ "value2", "value2", "value1", ]); expect(await client.lrem(key, -1, "value2")).toEqual(1); - expect(await client.lrange(key, 0, -1)).toEqual([ + checkSimple(await client.lrange(key, 0, -1)).toEqual([ "value2", "value1", ]); expect(await client.lrem(key, 0, "value2")).toEqual(1); - expect(await client.lrange(key, 0, -1)).toEqual(["value1"]); + checkSimple(await client.lrange(key, 0, -1)).toEqual([ + "value1", + ]); expect(await client.lrem("nonExistingKey", 2, "value")).toEqual( 0, ); @@ -979,8 +1040,8 @@ export function runBaseTests(config: { const key = uuidv4(); const valueList = ["value1", "value2", "value3", "value4"]; expect(await client.rpush(key, valueList)).toEqual(4); - expect(await client.rpop(key)).toEqual("value4"); - expect(await client.rpopCount(key, 2)).toEqual([ + checkSimple(await client.rpop(key)).toEqual("value4"); + checkSimple(await client.rpopCount(key, 2)).toEqual([ "value3", "value2", ]); @@ -995,7 +1056,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.rpush(key, ["bar"])).toThrow(); @@ -1017,6 +1078,41 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `rpushx list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const key3 = uuidv4(); + + expect(await client.rpush(key1, ["0"])).toEqual(1); + expect(await client.rpushx(key1, ["1", "2", "3"])).toEqual(4); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "0", + "1", + "2", + "3", + ]); + + expect(await client.rpushx(key2, ["1"])).toEqual(0); + checkSimple(await client.lrange(key2, 0, -1)).toEqual([]); + + // Key exists, but is not a list + checkSimple(await client.set(key3, "bar")); + await expect(client.rpushx(key3, ["_"])).rejects.toThrow( + RequestError, + ); + + // Empty element list + await expect(client.rpushx(key2, [])).rejects.toThrow( + RequestError, + ); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `sadd, srem, scard and smembers with existing set_%p`, async (protocol) => { @@ -1028,11 +1124,9 @@ export function runBaseTests(config: { await client.srem(key, ["member3", "nonExistingMember"]), ).toEqual(1); /// compare the 2 sets. - expect((await client.smembers(key)).sort()).toEqual([ - "member1", - "member2", - "member4", - ]); + checkSimple(await client.smembers(key)).toEqual( + new Set(["member1", "member2", "member4"]), + ); expect(await client.srem(key, ["member1"])).toEqual(1); expect(await client.scard(key)).toEqual(2); }, protocol); @@ -1040,6 +1134,86 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `smove test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}" + uuidv4(); + const key2 = "{key}" + uuidv4(); + const key3 = "{key}" + uuidv4(); + const string_key = "{key}" + uuidv4(); + const non_existing_key = "{key}" + uuidv4(); + + expect(await client.sadd(key1, ["1", "2", "3"])).toEqual(3); + expect(await client.sadd(key2, ["2", "3"])).toEqual(2); + + // move an element + expect(await client.smove(key1, key2, "1")); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["2", "3"]), + ); + checkSimple(await client.smembers(key2)).toEqual( + new Set(["1", "2", "3"]), + ); + + // moved element already exists in the destination set + expect(await client.smove(key2, key1, "2")); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["2", "3"]), + ); + checkSimple(await client.smembers(key2)).toEqual( + new Set(["1", "3"]), + ); + + // attempt to move from a non-existing key + expect(await client.smove(non_existing_key, key1, "4")).toEqual( + false, + ); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["2", "3"]), + ); + + // move to a new set + expect(await client.smove(key1, key3, "2")); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["3"]), + ); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["2"]), + ); + + // attempt to move a missing element + expect(await client.smove(key1, key3, "42")).toEqual(false); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["3"]), + ); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["2"]), + ); + + // move missing element to missing key + expect( + await client.smove(key1, non_existing_key, "42"), + ).toEqual(false); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["3"]), + ); + checkSimple(await client.type(non_existing_key)).toEqual( + "none", + ); + + // key exists, but it is not a set + checkSimple(await client.set(string_key, "value")).toEqual( + "OK", + ); + await expect( + client.smove(string_key, key1, "_"), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `srem, scard and smembers with non existing key_%p`, async (protocol) => { @@ -1048,7 +1222,9 @@ export function runBaseTests(config: { 0, ); expect(await client.scard("nonExistingKey")).toEqual(0); - expect(await client.smembers("nonExistingKey")).toEqual([]); + expect(await client.smembers("nonExistingKey")).toEqual( + new Set(), + ); }, protocol); }, config.timeout, @@ -1059,7 +1235,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); try { expect(await client.sadd(key, ["bar"])).toThrow(); @@ -1098,100 +1274,500 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `sismember test_%p`, + `sinter test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key1 = uuidv4(); - const key2 = uuidv4(); - expect(await client.sadd(key1, ["member1"])).toEqual(1); - expect(await client.sismember(key1, "member1")).toEqual(true); - expect( - await client.sismember(key1, "nonExistingMember"), - ).toEqual(false); - expect( - await client.sismember("nonExistingKey", "member1"), - ).toEqual(false); + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const non_existing_key = `{key}`; + const member1_list = ["a", "b", "c", "d"]; + const member2_list = ["c", "d", "e"]; + + // positive test case + expect(await client.sadd(key1, member1_list)).toEqual(4); + expect(await client.sadd(key2, member2_list)).toEqual(3); + checkSimple(await client.sinter([key1, key2])).toEqual( + new Set(["c", "d"]), + ); - expect(await client.set(key2, "foo")).toEqual("OK"); - await expect( - client.sismember(key2, "member1"), - ).rejects.toThrow(); + // invalid argument - key list must not be empty + try { + expect(await client.sinter([])).toThrow(); + } catch (e) { + expect((e as Error).message).toMatch( + "ResponseError: wrong number of arguments", + ); + } + + // non-existing key returns empty set + expect(await client.sinter([key1, non_existing_key])).toEqual( + new Set(), + ); + + // non-set key + checkSimple(await client.set(key2, "value")).toEqual("OK"); + + try { + expect(await client.sinter([key2])).toThrow(); + } catch (e) { + expect((e as Error).message).toMatch( + "Operation against a key holding the wrong kind of value", + ); + } }, protocol); }, config.timeout, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `spop and spopCount test_%p`, + `sintercard test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key = uuidv4(); - const members = ["member1", "member2", "member3"]; - expect(await client.sadd(key, members)).toEqual(3); + if (await checkIfServerVersionLessThan("7.0.0")) { + return; + } - const result1 = await client.spop(key); - expect(members).toContain(result1); + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const stringKey = `{key}-${uuidv4()}`; + const member1_list = ["a", "b", "c", "d"]; + const member2_list = ["b", "c", "d", "e"]; - const result2 = await client.spopCount(key, 2); - expect(members).toContain(result2?.[0]); - expect(members).toContain(result2?.[1]); - expect(result2).not.toContain(result1); + expect(await client.sadd(key1, member1_list)).toEqual(4); + expect(await client.sadd(key2, member2_list)).toEqual(4); - expect(await client.spop("nonExistingKey")).toEqual(null); - expect(await client.spopCount("nonExistingKey", 1)).toEqual([]); - }, protocol); - }, - config.timeout, - ); + expect(await client.sintercard([key1, key2])).toEqual(3); + + // returns limit as cardinality when the limit is reached partway through the computation + const limit = 2; + expect(await client.sintercard([key1, key2], limit)).toEqual( + limit, + ); + + // returns actual cardinality if limit is higher + expect(await client.sintercard([key1, key2], 4)).toEqual(3); + + // one of the keys is empty, intersection is empty, cardinality equals 0 + expect(await client.sintercard([key1, nonExistingKey])).toEqual( + 0, + ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `exists with existing keys, an non existing key_%p`, - async (protocol) => { - await runTest(async (client: BaseClient) => { - const key1 = uuidv4(); - const key2 = uuidv4(); - const value = uuidv4(); - expect(await client.set(key1, value)).toEqual("OK"); - expect(await client.exists([key1])).toEqual(1); - expect(await client.set(key2, value)).toEqual("OK"); expect( - await client.exists([key1, "nonExistingKey", key2]), - ).toEqual(2); - expect(await client.exists([key1, key1])).toEqual(2); + await client.sintercard([nonExistingKey, nonExistingKey]), + ).toEqual(0); + expect( + await client.sintercard( + [nonExistingKey, nonExistingKey], + 2, + ), + ).toEqual(0); + + // invalid argument - key list must not be empty + await expect(client.sintercard([])).rejects.toThrow( + RequestError, + ); + + // source key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.sintercard([key1, stringKey]), + ).rejects.toThrow(RequestError); }, protocol); }, config.timeout, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `unlink multiple existing keys and an non existing key_%p`, + `sinterstore test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key1 = "{key}" + uuidv4(); - const key2 = "{key}" + uuidv4(); - const key3 = "{key}" + uuidv4(); - const value = uuidv4(); - expect(await client.set(key1, value)).toEqual("OK"); - expect(await client.set(key2, value)).toEqual("OK"); - expect(await client.set(key3, value)).toEqual("OK"); + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const key3 = `{key}-3-${uuidv4()}`; + const nonExistingKey = `{key}-4-${uuidv4()}`; + const stringKey = `{key}-5-${uuidv4()}`; + const member1_list = ["a", "b", "c"]; + const member2_list = ["c", "d", "e"]; + + expect(await client.sadd(key1, member1_list)).toEqual(3); + expect(await client.sadd(key2, member2_list)).toEqual(3); + + // store in a new key + expect(await client.sinterstore(key3, [key1, key2])).toEqual(1); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["c"]), + ); + + // overwrite existing set, which is also a source set + expect(await client.sinterstore(key2, [key2, key3])).toEqual(1); + checkSimple(await client.smembers(key2)).toEqual( + new Set(["c"]), + ); + + // source set is the same as the existing set + expect(await client.sinterstore(key2, [key2])).toEqual(1); + checkSimple(await client.smembers(key2)).toEqual( + new Set(["c"]), + ); + + // intersection with non-existing key expect( - await client.unlink([key1, key2, "nonExistingKey", key3]), - ).toEqual(3); + await client.sinterstore(key1, [key2, nonExistingKey]), + ).toEqual(0); + checkSimple(await client.smembers(key1)).toEqual(new Set()); + + // invalid argument - key list must not be empty + await expect(client.sinterstore(key3, [])).rejects.toThrow(); + + // non-set key + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.sinterstore(key3, [stringKey]), + ).rejects.toThrow(); + + // overwrite non-set key + expect(await client.sinterstore(stringKey, [key2])).toEqual(1); + checkSimple(await client.smembers(stringKey)).toEqual( + new Set("c"), + ); }, protocol); }, config.timeout, ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `expire, pexpire and ttl with positive timeout_%p`, + `sdiff test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { - const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const stringKey = `{key}-3-${uuidv4()}`; + const nonExistingKey = `{key}-4-${uuidv4()}`; + const member1_list = ["a", "b", "c"]; + const member2_list = ["c", "d", "e"]; + + expect(await client.sadd(key1, member1_list)).toEqual(3); + expect(await client.sadd(key2, member2_list)).toEqual(3); + + checkSimple(await client.sdiff([key1, key2])).toEqual( + new Set(["a", "b"]), + ); + checkSimple(await client.sdiff([key2, key1])).toEqual( + new Set(["d", "e"]), + ); + + checkSimple(await client.sdiff([key1, nonExistingKey])).toEqual( + new Set(["a", "b", "c"]), + ); + checkSimple(await client.sdiff([nonExistingKey, key1])).toEqual( + new Set(), + ); + + // invalid arg - key list must not be empty + await expect(client.sdiff([])).rejects.toThrow(); + + // key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect(client.sdiff([stringKey])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sdiffstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const key3 = `{key}-3-${uuidv4()}`; + const stringKey = `{key}-4-${uuidv4()}`; + const nonExistingKey = `{key}-5-${uuidv4()}`; + const member1_list = ["a", "b", "c"]; + const member2_list = ["c", "d", "e"]; + + expect(await client.sadd(key1, member1_list)).toEqual(3); + expect(await client.sadd(key2, member2_list)).toEqual(3); + + // store diff in new key + expect(await client.sdiffstore(key3, [key1, key2])).toEqual(2); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["a", "b"]), + ); + + // overwrite existing set + expect(await client.sdiffstore(key3, [key2, key1])).toEqual(2); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["d", "e"]), + ); + + // overwrite one of the source sets + expect(await client.sdiffstore(key3, [key2, key3])).toEqual(1); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["c"]), + ); + + // diff between non-empty set and empty set + expect( + await client.sdiffstore(key3, [key1, nonExistingKey]), + ).toEqual(3); + checkSimple(await client.smembers(key3)).toEqual( + new Set(["a", "b", "c"]), + ); + + // diff between empty set and non-empty set + expect( + await client.sdiffstore(key3, [nonExistingKey, key1]), + ).toEqual(0); + checkSimple(await client.smembers(key3)).toEqual(new Set()); + + // invalid argument - key list must not be empty + await expect(client.sdiffstore(key3, [])).rejects.toThrow(); + + // source key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.sdiffstore(key3, [stringKey]), + ).rejects.toThrow(); + + // overwrite a key holding a non-set value + expect( + await client.sdiffstore(stringKey, [key1, key2]), + ).toEqual(2); + checkSimple(await client.smembers(stringKey)).toEqual( + new Set(["a", "b"]), + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sunion test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}:${uuidv4()}`; + const key2 = `{key}:${uuidv4()}`; + const stringKey = `{key}:${uuidv4()}`; + const nonExistingKey = `{key}:${uuidv4()}`; + const memberList1 = ["a", "b", "c"]; + const memberList2 = ["b", "c", "d", "e"]; + + expect(await client.sadd(key1, memberList1)).toEqual(3); + expect(await client.sadd(key2, memberList2)).toEqual(4); + checkSimple(await client.sunion([key1, key2])).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // invalid argument - key list must not be empty + await expect(client.sunion([])).rejects.toThrow(); + + // non-existing key returns the set of existing keys + checkSimple( + await client.sunion([key1, nonExistingKey]), + ).toEqual(new Set(memberList1)); + + // key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect(client.sunion([stringKey])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sunionstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}:${uuidv4()}`; + const key2 = `{key}:${uuidv4()}`; + const key3 = `{key}:${uuidv4()}`; + const key4 = `{key}:${uuidv4()}`; + const stringKey = `{key}:${uuidv4()}`; + const nonExistingKey = `{key}:${uuidv4()}`; + + expect(await client.sadd(key1, ["a", "b", "c"])).toEqual(3); + expect(await client.sadd(key2, ["c", "d", "e"])).toEqual(3); + expect(await client.sadd(key3, ["e", "f", "g"])).toEqual(3); + + // store union in new key + expect(await client.sunionstore(key4, [key1, key2])).toEqual(5); + checkSimple(await client.smembers(key4)).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // overwrite existing set + expect(await client.sunionstore(key1, [key4, key2])).toEqual(5); + checkSimple(await client.smembers(key1)).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // overwrite one of the source keys + expect(await client.sunionstore(key2, [key4, key2])).toEqual(5); + checkSimple(await client.smembers(key2)).toEqual( + new Set(["a", "b", "c", "d", "e"]), + ); + + // union with a non-existing key + expect( + await client.sunionstore(key2, [nonExistingKey]), + ).toEqual(0); + expect(await client.smembers(key2)).toEqual(new Set()); + + // invalid argument - key list must not be empty + await expect(client.sunionstore(key4, [])).rejects.toThrow(); + + // key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.sunionstore(key4, [stringKey, key1]), + ).rejects.toThrow(); + + // overwrite destination when destination is not a set + expect( + await client.sunionstore(stringKey, [key1, key3]), + ).toEqual(7); + checkSimple(await client.smembers(stringKey)).toEqual( + new Set(["a", "b", "c", "d", "e", "f", "g"]), + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `sismember test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + expect(await client.sadd(key1, ["member1"])).toEqual(1); + expect(await client.sismember(key1, "member1")).toEqual(true); + expect( + await client.sismember(key1, "nonExistingMember"), + ).toEqual(false); + expect( + await client.sismember("nonExistingKey", "member1"), + ).toEqual(false); + + checkSimple(await client.set(key2, "foo")).toEqual("OK"); + await expect( + client.sismember(key2, "member1"), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `smismember test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key = uuidv4(); + const stringKey = uuidv4(); + const nonExistingKey = uuidv4(); + + expect(await client.sadd(key, ["a", "b"])).toEqual(2); + expect(await client.smismember(key, ["b", "c"])).toEqual([ + true, + false, + ]); + + expect(await client.smismember(nonExistingKey, ["b"])).toEqual([ + false, + ]); + + // invalid argument - member list must not be empty + await expect(client.smismember(key, [])).rejects.toThrow( + RequestError, + ); + + // key exists, but it is not a set + checkSimple(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.smismember(stringKey, ["a"]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `spop and spopCount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + let members = ["member1", "member2", "member3"]; + expect(await client.sadd(key, members)).toEqual(3); + + const result1 = await client.spop(key); + expect(members).toContain(intoString(result1)); + + members = members.filter((item) => item != result1); + const result2 = await client.spopCount(key, 2); + expect(intoString(result2)).toEqual(intoString(members)); + expect(await client.spop("nonExistingKey")).toEqual(null); + expect(await client.spopCount("nonExistingKey", 1)).toEqual( + new Set(), + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `exists with existing keys, an non existing key_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const value = uuidv4(); + checkSimple(await client.set(key1, value)).toEqual("OK"); + expect(await client.exists([key1])).toEqual(1); + checkSimple(await client.set(key2, value)).toEqual("OK"); + expect( + await client.exists([key1, "nonExistingKey", key2]), + ).toEqual(2); + expect(await client.exists([key1, key1])).toEqual(2); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `unlink multiple existing keys and an non existing key_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = "{key}" + uuidv4(); + const key2 = "{key}" + uuidv4(); + const key3 = "{key}" + uuidv4(); + const value = uuidv4(); + checkSimple(await client.set(key1, value)).toEqual("OK"); + checkSimple(await client.set(key2, value)).toEqual("OK"); + checkSimple(await client.set(key3, value)).toEqual("OK"); + expect( + await client.unlink([key1, key2, "nonExistingKey", key3]), + ).toEqual(3); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `expire, pexpire and ttl with positive timeout_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect(await client.expire(key, 10)).toEqual(true); expect(await client.ttl(key)).toBeLessThanOrEqual(10); /// set command clears the timeout. - expect(await client.set(key, "bar")).toEqual("OK"); + checkSimple(await client.set(key, "bar")).toEqual("OK"); const versionLessThan = await checkIfServerVersionLessThan("7.0.0"); @@ -1233,7 +1809,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect( await client.expireAt( key, @@ -1264,7 +1840,7 @@ export function runBaseTests(config: { expect(await client.ttl(key)).toBeLessThanOrEqual(50); /// set command clears the timeout. - expect(await client.set(key, "bar")).toEqual("OK"); + checkSimple(await client.set(key, "bar")).toEqual("OK"); if (!versionLessThan) { expect( @@ -1285,14 +1861,14 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect(await client.ttl(key)).toEqual(-1); expect(await client.expire(key, -10)).toEqual(true); expect(await client.ttl(key)).toEqual(-2); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect(await client.pexpire(key, -10000)).toEqual(true); expect(await client.ttl(key)).toEqual(-2); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect( await client.expireAt( key, @@ -1300,7 +1876,7 @@ export function runBaseTests(config: { ), ).toEqual(true); expect(await client.ttl(key)).toEqual(-2); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect( await client.pexpireAt( key, @@ -1340,18 +1916,61 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `script test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = Buffer.from(uuidv4()); + const key2 = Buffer.from(uuidv4()); + + let script = new Script(Buffer.from("return 'Hello'")); + checkSimple(await client.invokeScript(script)).toEqual("Hello"); + + script = new Script( + Buffer.from("return redis.call('SET', KEYS[1], ARGV[1])"), + ); + checkSimple( + await client.invokeScript(script, { + keys: [key1], + args: [Buffer.from("value1")], + }), + ).toEqual("OK"); + + /// Reuse the same script with different parameters. + checkSimple( + await client.invokeScript(script, { + keys: [key2], + args: [Buffer.from("value2")], + }), + ).toEqual("OK"); + + script = new Script( + Buffer.from("return redis.call('GET', KEYS[1])"), + ); + checkSimple( + await client.invokeScript(script, { keys: [key1] }), + ).toEqual("value1"); + + checkSimple( + await client.invokeScript(script, { keys: [key2] }), + ).toEqual("value2"); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `script test_binary_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key1 = uuidv4(); const key2 = uuidv4(); let script = new Script("return 'Hello'"); - expect(await client.invokeScript(script)).toEqual("Hello"); + checkSimple(await client.invokeScript(script)).toEqual("Hello"); script = new Script( "return redis.call('SET', KEYS[1], ARGV[1])", ); - expect( + checkSimple( await client.invokeScript(script, { keys: [key1], args: ["value1"], @@ -1359,7 +1978,7 @@ export function runBaseTests(config: { ).toEqual("OK"); /// Reuse the same script with different parameters. - expect( + checkSimple( await client.invokeScript(script, { keys: [key2], args: ["value2"], @@ -1367,11 +1986,11 @@ export function runBaseTests(config: { ).toEqual("OK"); script = new Script("return redis.call('GET', KEYS[1])"); - expect( + checkSimple( await client.invokeScript(script, { keys: [key1] }), ).toEqual("value1"); - expect( + checkSimple( await client.invokeScript(script, { keys: [key2] }), ).toEqual("value2"); }, protocol); @@ -1509,6 +2128,49 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zintercard test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + if (await checkIfServerVersionLessThan("7.0.0")) { + return; + } + + const key1 = `{key}:${uuidv4()}`; + const key2 = `{key}:${uuidv4()}`; + const stringKey = `{key}:${uuidv4()}`; + const nonExistingKey = `{key}:${uuidv4()}`; + const memberScores1 = { one: 1, two: 2, three: 3 }; + const memberScores2 = { two: 2, three: 3, four: 4 }; + + expect(await client.zadd(key1, memberScores1)).toEqual(3); + expect(await client.zadd(key2, memberScores2)).toEqual(3); + + expect(await client.zintercard([key1, key2])).toEqual(2); + expect(await client.zintercard([key1, nonExistingKey])).toEqual( + 0, + ); + + expect(await client.zintercard([key1, key2], 0)).toEqual(2); + expect(await client.zintercard([key1, key2], 1)).toEqual(1); + expect(await client.zintercard([key1, key2], 2)).toEqual(2); + + // invalid argument - key list must not be empty + await expect(client.zintercard([])).rejects.toThrow(); + + // invalid argument - limit must be non-negative + await expect( + client.zintercard([key1, key2], -1), + ).rejects.toThrow(); + + // key exists, but it is not a sorted set + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect(client.zintercard([stringKey])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zscore test_%p`, async (protocol) => { @@ -1525,7 +2187,7 @@ export function runBaseTests(config: { await client.zscore("nonExistingKey", "nonExistingMember"), ).toEqual(null); - expect(await client.set(key2, "foo")).toEqual("OK"); + checkSimple(await client.set(key2, "foo")).toEqual("OK"); await expect(client.zscore(key2, "foo")).rejects.toThrow(); }, protocol); }, @@ -1579,7 +2241,7 @@ export function runBaseTests(config: { ), ).toEqual(0); - expect(await client.set(key2, "foo")).toEqual("OK"); + checkSimple(await client.set(key2, "foo")).toEqual("OK"); await expect( client.zcount(key2, "negativeInfinity", "positiveInfinity"), ).rejects.toThrow(); @@ -1596,13 +2258,22 @@ export function runBaseTests(config: { const membersScores = { one: 1, two: 2, three: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - expect(await client.zrange(key, { start: 0, stop: 1 })).toEqual( - ["one", "two"], - ); - expect( - await client.zrangeWithScores(key, { start: 0, stop: -1 }), - ).toEqual({ one: 1.0, two: 2.0, three: 3.0 }); + checkSimple( + await client.zrange(key, { start: 0, stop: 1 }), + ).toEqual(["one", "two"]); + const result = await client.zrangeWithScores(key, { + start: 0, + stop: -1, + }); + expect( + compareMaps(result, { + one: 1.0, + two: 2.0, + three: 3.0, + }), + ).toBe(true); + checkSimple( await client.zrange(key, { start: 0, stop: 1 }, true), ).toEqual(["three", "two"]); expect(await client.zrange(key, { start: 3, stop: 1 })).toEqual( @@ -1624,23 +2295,27 @@ export function runBaseTests(config: { const membersScores = { one: 1, two: 2, three: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - expect( + checkSimple( await client.zrange(key, { start: "negativeInfinity", stop: { value: 3, isInclusive: false }, type: "byScore", }), ).toEqual(["one", "two"]); + const result = await client.zrangeWithScores(key, { + start: "negativeInfinity", + stop: "positiveInfinity", + type: "byScore", + }); expect( - await client.zrangeWithScores(key, { - start: "negativeInfinity", - stop: "positiveInfinity", - type: "byScore", + compareMaps(result, { + one: 1.0, + two: 2.0, + three: 3.0, }), - ).toEqual({ one: 1.0, two: 2.0, three: 3.0 }); - - expect( + ).toBe(true); + checkSimple( await client.zrange( key, { @@ -1652,7 +2327,7 @@ export function runBaseTests(config: { ), ).toEqual(["two", "one"]); - expect( + checkSimple( await client.zrange(key, { start: "negativeInfinity", stop: "positiveInfinity", @@ -1713,7 +2388,7 @@ export function runBaseTests(config: { const membersScores = { a: 1, b: 2, c: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - expect( + checkSimple( await client.zrange(key, { start: "negativeInfinity", stop: { value: "c", isInclusive: false }, @@ -1721,7 +2396,7 @@ export function runBaseTests(config: { }), ).toEqual(["a", "b"]); - expect( + checkSimple( await client.zrange(key, { start: "negativeInfinity", stop: "positiveInfinity", @@ -1730,7 +2405,7 @@ export function runBaseTests(config: { }), ).toEqual(["b", "c"]); - expect( + checkSimple( await client.zrange( key, { @@ -1799,30 +2474,167 @@ export function runBaseTests(config: { config.timeout, ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + // Zinterstore command tests + async function zinterstoreWithAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the MAX score of elements + expect(await client.zinterstore(key3, [key1, key2], "MAX")).toEqual(2); + const zinterstoreMapMax = await client.zrangeWithScores(key3, range); + const expectedMapMax = { + one: 2, + two: 3, + }; + expect(compareMaps(zinterstoreMapMax, expectedMapMax)).toBe(true); + + // Intersection results are aggregated by the MIN score of elements + expect(await client.zinterstore(key3, [key1, key2], "MIN")).toEqual(2); + const zinterstoreMapMin = await client.zrangeWithScores(key3, range); + const expectedMapMin = { + one: 1, + two: 2, + }; + expect(compareMaps(zinterstoreMapMin, expectedMapMin)).toBe(true); + + // Intersection results are aggregated by the SUM score of elements + expect(await client.zinterstore(key3, [key1, key2], "SUM")).toEqual(2); + const zinterstoreMapSum = await client.zrangeWithScores(key3, range); + const expectedMapSum = { + one: 3, + two: 5, + }; + expect(compareMaps(zinterstoreMapSum, expectedMapSum)).toBe(true); + } + + async function zinterstoreBasicTest(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + expect(await client.zinterstore(key3, [key1, key2])).toEqual(2); + const zinterstoreMap = await client.zrangeWithScores(key3, range); + const expectedMap = { + one: 3, + two: 5, + }; + expect(compareMaps(zinterstoreMap, expectedMap)).toBe(true); + } + + async function zinterstoreWithWeightsAndAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + expect( + await client.zinterstore( + key3, + [ + [key1, 2.0], + [key2, 2.0], + ], + "SUM", + ), + ).toEqual(2); + const zinterstoreMapMultiplied = await client.zrangeWithScores( + key3, + range, + ); + const expectedMapMultiplied = { + one: 6, + two: 10, + }; + expect( + compareMaps(zinterstoreMapMultiplied, expectedMapMultiplied), + ).toBe(true); + } + + async function zinterstoreEmptyCases(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + // Non existing key + expect( + await client.zinterstore(key2, [ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual(0); + + // Empty list check + await expect(client.zinterstore("{xyz}", [])).rejects.toThrow(); + } + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinterstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + await zinterstoreBasicTest(client); + await zinterstoreWithAggregation(client); + await zinterstoreWithWeightsAndAggregation(client); + await zinterstoreEmptyCases(client); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `type test_%p`, async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "value")).toEqual("OK"); - expect(await client.type(key)).toEqual("string"); - expect(await client.del([key])).toEqual(1); + checkSimple(await client.set(key, "value")).toEqual("OK"); + checkSimple(await client.type(key)).toEqual("string"); + checkSimple(await client.del([key])).toEqual(1); - expect(await client.lpush(key, ["value"])).toEqual(1); - expect(await client.type(key)).toEqual("list"); - expect(await client.del([key])).toEqual(1); + checkSimple(await client.lpush(key, ["value"])).toEqual(1); + checkSimple(await client.type(key)).toEqual("list"); + checkSimple(await client.del([key])).toEqual(1); - expect(await client.sadd(key, ["value"])).toEqual(1); - expect(await client.type(key)).toEqual("set"); - expect(await client.del([key])).toEqual(1); + checkSimple(await client.sadd(key, ["value"])).toEqual(1); + checkSimple(await client.type(key)).toEqual("set"); + checkSimple(await client.del([key])).toEqual(1); - expect(await client.zadd(key, { member: 1.0 })).toEqual(1); - expect(await client.type(key)).toEqual("zset"); - expect(await client.del([key])).toEqual(1); + checkSimple(await client.zadd(key, { member: 1.0 })).toEqual(1); + checkSimple(await client.type(key)).toEqual("zset"); + checkSimple(await client.del([key])).toEqual(1); - expect(await client.hset(key, { field: "value" })).toEqual(1); - expect(await client.type(key)).toEqual("hash"); - expect(await client.del([key])).toEqual(1); + checkSimple(await client.hset(key, { field: "value" })).toEqual( + 1, + ); + checkSimple(await client.type(key)).toEqual("hash"); + checkSimple(await client.del([key])).toEqual(1); await client.customCommand([ "XADD", @@ -1831,10 +2643,9 @@ export function runBaseTests(config: { "field", "value", ]); - expect(await client.type(key)).toEqual("stream"); - expect(await client.del([key])).toEqual(1); - - expect(await client.type(key)).toEqual("none"); + checkSimple(await client.type(key)).toEqual("stream"); + checkSimple(await client.del([key])).toEqual(1); + checkSimple(await client.type(key)).toEqual("none"); }, protocol); }, config.timeout, @@ -1845,7 +2656,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const message = uuidv4(); - expect(await client.echo(message)).toEqual(message); + checkSimple(await client.echo(message)).toEqual(message); }, protocol); }, config.timeout, @@ -1858,8 +2669,8 @@ export function runBaseTests(config: { const key1 = uuidv4(); const key1Value = uuidv4(); const key1ValueLength = key1Value.length; - expect(await client.set(key1, key1Value)).toEqual("OK"); - expect(await client.strlen(key1)).toEqual(key1ValueLength); + checkSimple(await client.set(key1, key1Value)).toEqual("OK"); + checkSimple(await client.strlen(key1)).toEqual(key1ValueLength); expect(await client.strlen("nonExistKey")).toEqual(0); @@ -1893,8 +2704,12 @@ export function runBaseTests(config: { listKey2Value, ]), ).toEqual(2); - expect(await client.lindex(listName, 0)).toEqual(listKey2Value); - expect(await client.lindex(listName, 1)).toEqual(listKey1Value); + checkSimple(await client.lindex(listName, 0)).toEqual( + listKey2Value, + ); + checkSimple(await client.lindex(listName, 1)).toEqual( + listKey1Value, + ); expect(await client.lindex("notExsitingList", 1)).toEqual(null); expect(await client.lindex(listName, 3)).toEqual(null); }, protocol); @@ -1902,6 +2717,69 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `linsert test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const stringKey = uuidv4(); + const nonExistingKey = uuidv4(); + + expect(await client.lpush(key1, ["4", "3", "2", "1"])).toEqual( + 4, + ); + expect( + await client.linsert( + key1, + InsertPosition.Before, + "2", + "1.5", + ), + ).toEqual(5); + expect( + await client.linsert( + key1, + InsertPosition.After, + "3", + "3.5", + ), + ).toEqual(6); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "1", + "1.5", + "2", + "3", + "3.5", + "4", + ]); + + expect( + await client.linsert( + key1, + InsertPosition.Before, + "nonExistingPivot", + "4", + ), + ).toEqual(-1); + expect( + await client.linsert( + nonExistingKey, + InsertPosition.Before, + "pivot", + "elem", + ), + ).toEqual(0); + + // key exists, but it is not a list + expect(await client.set(stringKey, "value")).toEqual("OK"); + await expect( + client.linsert(stringKey, InsertPosition.Before, "a", "b"), + ).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zpopmin test_%p`, async (protocol) => { @@ -1910,12 +2788,15 @@ export function runBaseTests(config: { const membersScores = { a: 1, b: 2, c: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); expect(await client.zpopmin(key)).toEqual({ a: 1.0 }); - expect(await client.zpopmin(key, 3)).toEqual({ - b: 2.0, - c: 3.0, - }); + + expect( + compareMaps(await client.zpopmin(key, 3), { + b: 2.0, + c: 3.0, + }), + ).toBe(true); expect(await client.zpopmin(key)).toEqual({}); - expect(await client.set(key, "value")).toEqual("OK"); + checkSimple(await client.set(key, "value")).toEqual("OK"); await expect(client.zpopmin(key)).rejects.toThrow(); expect(await client.zpopmin("notExsitingKey")).toEqual({}); }, protocol); @@ -1931,12 +2812,15 @@ export function runBaseTests(config: { const membersScores = { a: 1, b: 2, c: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); expect(await client.zpopmax(key)).toEqual({ c: 3.0 }); - expect(await client.zpopmax(key, 3)).toEqual({ - b: 2.0, - a: 1.0, - }); + + expect( + compareMaps(await client.zpopmax(key, 3), { + b: 2.0, + a: 1.0, + }), + ).toBe(true); expect(await client.zpopmax(key)).toEqual({}); - expect(await client.set(key, "value")).toEqual("OK"); + checkSimple(await client.set(key, "value")).toEqual("OK"); await expect(client.zpopmax(key)).rejects.toThrow(); expect(await client.zpopmax("notExsitingKey")).toEqual({}); }, protocol); @@ -1951,7 +2835,7 @@ export function runBaseTests(config: { const key = uuidv4(); expect(await client.pttl(key)).toEqual(-2); - expect(await client.set(key, "value")).toEqual("OK"); + checkSimple(await client.set(key, "value")).toEqual("OK"); expect(await client.pttl(key)).toEqual(-1); expect(await client.expire(key, 10)).toEqual(true); @@ -2027,7 +2911,7 @@ export function runBaseTests(config: { null, ); - expect(await client.set(key2, "value")).toEqual("OK"); + checkSimple(await client.set(key2, "value")).toEqual("OK"); await expect(client.zrank(key2, "member")).rejects.toThrow(); }, protocol); }, @@ -2041,7 +2925,7 @@ export function runBaseTests(config: { await client.rpush("brpop-test", ["foo", "bar", "baz"]), ).toEqual(3); // Test basic usage - expect(await client.brpop(["brpop-test"], 0.1)).toEqual([ + checkSimple(await client.brpop(["brpop-test"], 0.1)).toEqual([ "brpop-test", "baz", ]); @@ -2049,6 +2933,59 @@ export function runBaseTests(config: { expect(await client.del(["brpop-test"])).toEqual(1); // Test null return when key doesn't exist expect(await client.brpop(["brpop-test"], 0.1)).toEqual(null); + // key exists, but it is not a list + await client.set("foo", "bar"); + await expect(client.brpop(["foo"], 0.1)).rejects.toThrow(); + + // Same-slot requirement + if (client instanceof GlideClusterClient) { + try { + expect( + await client.brpop(["abc", "zxy", "lkn"], 0.1), + ).toThrow(); + } catch (e) { + expect((e as Error).message.toLowerCase()).toMatch( + "crossslot", + ); + } + } + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `test blpop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + expect( + await client.rpush("blpop-test", ["foo", "bar", "baz"]), + ).toEqual(3); + // Test basic usage + checkSimple(await client.blpop(["blpop-test"], 0.1)).toEqual([ + "blpop-test", + "foo", + ]); + // Delete all values from list + expect(await client.del(["blpop-test"])).toEqual(1); + // Test null return when key doesn't exist + expect(await client.blpop(["blpop-test"], 0.1)).toEqual(null); + // key exists, but it is not a list + await client.set("foo", "bar"); + await expect(client.blpop(["foo"], 0.1)).rejects.toThrow(); + + // Same-slot requirement + if (client instanceof GlideClusterClient) { + try { + expect( + await client.blpop(["abc", "zxy", "lkn"], 0.1), + ).toThrow(); + } catch (e) { + expect((e as Error).message.toLowerCase()).toMatch( + "crossslot", + ); + } + } }, protocol); }, config.timeout, @@ -2059,7 +2996,7 @@ export function runBaseTests(config: { async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); - expect(await client.set(key, "foo")).toEqual("OK"); + checkSimple(await client.set(key, "foo")).toEqual("OK"); expect(await client.persist(key)).toEqual(false); expect(await client.expire(key, 10)).toEqual(true); @@ -2070,10 +3007,12 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `streams add and trim test_%p`, - async () => { + `streams add, trim, and len test_%p`, + async (protocol) => { await runTest(async (client: BaseClient) => { const key = uuidv4(); + const nonExistingKey = uuidv4(); + const stringKey = uuidv4(); const field1 = uuidv4(); const field2 = uuidv4(); @@ -2097,14 +3036,14 @@ export function runBaseTests(config: { ], { id: "0-1" }, ); - expect(timestamp1).toEqual("0-1"); + checkSimple(timestamp1).toEqual("0-1"); expect( await client.xadd(key, [ [field1, "foo2"], [field2, "bar2"], ]), ).not.toBeNull(); - expect(await client.customCommand(["XLEN", key])).toEqual(2); + expect(await client.xlen(key)).toEqual(2); // this will trim the first entry. const id = await client.xadd( @@ -2122,7 +3061,7 @@ export function runBaseTests(config: { }, ); expect(id).not.toBeNull(); - expect(await client.customCommand(["XLEN", key])).toEqual(2); + expect(await client.xlen(key)).toEqual(2); // this will trim the 2nd entry. expect( @@ -2141,7 +3080,7 @@ export function runBaseTests(config: { }, ), ).not.toBeNull(); - expect(await client.customCommand(["XLEN", key])).toEqual(2); + expect(await client.xlen(key)).toEqual(2); expect( await client.xtrim(key, { @@ -2150,8 +3089,39 @@ export function runBaseTests(config: { exact: true, }), ).toEqual(1); - expect(await client.customCommand(["XLEN", key])).toEqual(1); - }, ProtocolVersion.RESP2); + expect(await client.xlen(key)).toEqual(1); + + expect( + await client.xtrim(key, { + method: "maxlen", + threshold: 0, + exact: true, + }), + ).toEqual(1); + // Unlike other Redis collection types, stream keys still exist even after removing all entries + expect(await client.exists([key])).toEqual(1); + expect(await client.xlen(key)).toEqual(0); + + expect( + await client.xtrim(nonExistingKey, { + method: "maxlen", + threshold: 1, + exact: true, + }), + ).toEqual(0); + expect(await client.xlen(nonExistingKey)).toEqual(0); + + // key exists, but it is not a stream + expect(await client.set(stringKey, "foo")).toEqual("OK"); + await expect( + client.xtrim(stringKey, { + method: "maxlen", + threshold: 1, + exact: true, + }), + ).rejects.toThrow(); + await expect(client.xlen(stringKey)).rejects.toThrow(); + }, protocol); }, config.timeout, ); @@ -2203,7 +3173,6 @@ export function runBaseTests(config: { expect(Number(result?.at(0))).toBeGreaterThan(now); // Test its not more than 1 second expect(Number(result?.at(1))).toBeLessThan(1000000); - client.close(); }, protocol); }, ); @@ -2252,16 +3221,19 @@ export function runBaseTests(config: { ); const expected = { - [key1]: [ - [timestamp_1_2, [field1, "foo2"]], - [timestamp_1_3, [field1, "foo3", field3, "barvaz3"]], - ], - [key2]: [ - [timestamp_2_2, ["bar", "bar2"]], - [timestamp_2_3, ["bar", "bar3"]], - ], + [key1]: { + [timestamp_1_2 as string]: [[field1, "foo2"]], + [timestamp_1_3 as string]: [ + [field1, "foo3"], + [field3, "barvaz3"], + ], + }, + [key2]: { + [timestamp_2_2 as string]: [["bar", "bar2"]], + [timestamp_2_3 as string]: [["bar", "bar3"]], + }, }; - expect(result).toEqual(expected); + checkSimple(result).toEqual(expected); }, ProtocolVersion.RESP2); }, config.timeout, @@ -2277,10 +3249,773 @@ export function runBaseTests(config: { await client.set(key, "value"); await client.rename(key, newKey); const result = await client.get(newKey); - expect(result).toEqual("value"); - // If key doesn't exist it should throw, it also test that key has succfully been renamed + checkSimple(result).toEqual("value"); + // If key doesn't exist it should throw, it also test that key has successfully been renamed await expect(client.rename(key, newKey)).rejects.toThrow(); - client.close(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "renamenx test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const key3 = `{key}-3-${uuidv4()}`; + + // renamenx missing key + try { + expect(await client.renamenx(key1, key2)).toThrow(); + } catch (e) { + expect((e as Error).message).toMatch("no such key"); + } + + // renamenx a string + await client.set(key1, "key1"); + await client.set(key3, "key3"); + // Test that renamenx can rename key1 to key2 (non-existing value) + checkSimple(await client.renamenx(key1, key2)).toEqual(true); + // sanity check + checkSimple(await client.get(key2)).toEqual("key1"); + // Test that renamenx doesn't rename key2 to key3 (with an existing value) + checkSimple(await client.renamenx(key2, key3)).toEqual(false); + // sanity check + checkSimple(await client.get(key3)).toEqual("key3"); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "pfadd test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + checkSimple(await client.pfadd(key, [])).toEqual(1); + checkSimple(await client.pfadd(key, ["one", "two"])).toEqual(1); + checkSimple(await client.pfadd(key, ["two"])).toEqual(0); + checkSimple(await client.pfadd(key, [])).toEqual(0); + + // key exists, but it is not a HyperLogLog + checkSimple(await client.set("foo", "value")).toEqual("OK"); + await expect(client.pfadd("foo", [])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "pfcount test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-1-${uuidv4()}`; + const key2 = `{key}-2-${uuidv4()}`; + const key3 = `{key}-3-${uuidv4()}`; + const stringKey = `{key}-4-${uuidv4()}`; + const nonExistingKey = `{key}-5-${uuidv4()}`; + + expect(await client.pfadd(key1, ["a", "b", "c"])).toEqual(1); + expect(await client.pfadd(key2, ["b", "c", "d"])).toEqual(1); + expect(await client.pfcount([key1])).toEqual(3); + expect(await client.pfcount([key2])).toEqual(3); + expect(await client.pfcount([key1, key2])).toEqual(4); + expect( + await client.pfcount([key1, key2, nonExistingKey]), + ).toEqual(4); + + // empty HyperLogLog data set + expect(await client.pfadd(key3, [])).toEqual(1); + expect(await client.pfcount([key3])).toEqual(0); + + // invalid argument - key list must not be empty + try { + expect(await client.pfcount([])).toThrow(); + } catch (e) { + expect((e as Error).message).toMatch( + "ResponseError: wrong number of arguments", + ); + } + + // key exists, but it is not a HyperLogLog + expect(await client.set(stringKey, "value")).toEqual("OK"); + await expect(client.pfcount([stringKey])).rejects.toThrow(); + }, protocol); + }, + config.timeout, + ); + + // Set command tests + + async function setWithExpiryOptions(client: BaseClient) { + const key = uuidv4(); + const value = uuidv4(); + const setResWithExpirySetMilli = await client.set(key, value, { + expiry: { + type: "milliseconds", + count: 500, + }, + }); + checkSimple(setResWithExpirySetMilli).toEqual("OK"); + const getWithExpirySetMilli = await client.get(key); + checkSimple(getWithExpirySetMilli).toEqual(value); + + const setResWithExpirySec = await client.set(key, value, { + expiry: { + type: "seconds", + count: 1, + }, + }); + checkSimple(setResWithExpirySec).toEqual("OK"); + const getResWithExpirySec = await client.get(key); + checkSimple(getResWithExpirySec).toEqual(value); + + const setWithUnixSec = await client.set(key, value, { + expiry: { + type: "unixSeconds", + count: Math.floor(Date.now() / 1000) + 1, + }, + }); + checkSimple(setWithUnixSec).toEqual("OK"); + const getWithUnixSec = await client.get(key); + checkSimple(getWithUnixSec).toEqual(value); + + const setResWithExpiryKeep = await client.set(key, value, { + expiry: "keepExisting", + }); + checkSimple(setResWithExpiryKeep).toEqual("OK"); + const getResWithExpiryKeep = await client.get(key); + checkSimple(getResWithExpiryKeep).toEqual(value); + // wait for the key to expire base on the previous set + let sleep = new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep; + const getResExpire = await client.get(key); + // key should have expired + checkSimple(getResExpire).toEqual(null); + const setResWithExpiryWithUmilli = await client.set(key, value, { + expiry: { + type: "unixMilliseconds", + count: Date.now() + 1000, + }, + }); + checkSimple(setResWithExpiryWithUmilli).toEqual("OK"); + // wait for the key to expire + sleep = new Promise((resolve) => setTimeout(resolve, 1001)); + await sleep; + const getResWithExpiryWithUmilli = await client.get(key); + // key should have expired + checkSimple(getResWithExpiryWithUmilli).toEqual(null); + } + + async function setWithOnlyIfExistOptions(client: BaseClient) { + const key = uuidv4(); + const value = uuidv4(); + const setKey = await client.set(key, value); + checkSimple(setKey).toEqual("OK"); + const getRes = await client.get(key); + checkSimple(getRes).toEqual(value); + const setExistingKeyRes = await client.set(key, value, { + conditionalSet: "onlyIfExists", + }); + checkSimple(setExistingKeyRes).toEqual("OK"); + const getExistingKeyRes = await client.get(key); + checkSimple(getExistingKeyRes).toEqual(value); + + const notExistingKeyRes = await client.set(key + 1, value, { + conditionalSet: "onlyIfExists", + }); + // key does not exist, so it should not be set + checkSimple(notExistingKeyRes).toEqual(null); + const getNotExistingKey = await client.get(key + 1); + // key should not have been set + checkSimple(getNotExistingKey).toEqual(null); + } + + async function setWithOnlyIfNotExistOptions(client: BaseClient) { + const key = uuidv4(); + const value = uuidv4(); + const notExistingKeyRes = await client.set(key, value, { + conditionalSet: "onlyIfDoesNotExist", + }); + // key does not exist, so it should be set + checkSimple(notExistingKeyRes).toEqual("OK"); + const getNotExistingKey = await client.get(key); + // key should have been set + checkSimple(getNotExistingKey).toEqual(value); + + const existingKeyRes = await client.set(key, value, { + conditionalSet: "onlyIfDoesNotExist", + }); + // key exists, so it should not be set + checkSimple(existingKeyRes).toEqual(null); + const getExistingKey = await client.get(key); + // key should not have been set + checkSimple(getExistingKey).toEqual(value); + } + + async function setWithGetOldOptions(client: BaseClient) { + const key = uuidv4(); + const value = uuidv4(); + + const setResGetNotExistOld = await client.set(key, value, { + returnOldValue: true, + }); + // key does not exist, so old value should be null + checkSimple(setResGetNotExistOld).toEqual(null); + // key should have been set + const getResGetNotExistOld = await client.get(key); + checkSimple(getResGetNotExistOld).toEqual(value); + + const setResGetExistOld = await client.set(key, value, { + returnOldValue: true, + }); + // key exists, so old value should be returned + checkSimple(setResGetExistOld).toEqual(value); + // key should have been set + const getResGetExistOld = await client.get(key); + checkSimple(getResGetExistOld).toEqual(value); + } + + async function setWithAllOptions(client: BaseClient) { + const key = uuidv4(); + const value = uuidv4(); + + // set with multiple options: + // * only apply SET if the key already exists + // * expires after 1 second + // * returns the old value + const setResWithAllOptions = await client.set(key, value, { + expiry: { + type: "unixSeconds", + count: Math.floor(Date.now() / 1000) + 1, + }, + conditionalSet: "onlyIfExists", + returnOldValue: true, + }); + // key does not exist, so old value should be null + expect(setResWithAllOptions).toEqual(null); + // key does not exist, so SET should not have applied + expect(await client.get(key)).toEqual(null); + } + + async function testSetWithAllCombination(client: BaseClient) { + const key = uuidv4(); + const value = uuidv4(); + const count = 2; + const expiryCombination = [ + { type: "seconds", count }, + { type: "unixSeconds", count }, + { type: "unixMilliseconds", count }, + { type: "milliseconds", count }, + "keepExisting", + ]; + let exist = false; + + for (const expiryVal of expiryCombination) { + const setRes = await client.set(key, value, { + expiry: expiryVal as + | "keepExisting" + | { + type: + | "seconds" + | "milliseconds" + | "unixSeconds" + | "unixMilliseconds"; + count: number; + }, + conditionalSet: "onlyIfDoesNotExist", + }); + + if (exist == false) { + checkSimple(setRes).toEqual("OK"); + exist = true; + } else { + checkSimple(setRes).toEqual(null); + } + + const getRes = await client.get(key); + checkSimple(getRes).toEqual(value); + } + + for (const expiryVal of expiryCombination) { + const setRes = await client.set(key, value, { + expiry: expiryVal as + | "keepExisting" + | { + type: + | "seconds" + | "milliseconds" + | "unixSeconds" + | "unixMilliseconds"; + count: number; + }, + + conditionalSet: "onlyIfExists", + returnOldValue: true, + }); + + expect(setRes).toBeDefined(); + } + } + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Set commands with options test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + await setWithExpiryOptions(client); + await setWithOnlyIfExistOptions(client); + await setWithOnlyIfNotExistOptions(client); + await setWithGetOldOptions(client); + await setWithAllOptions(client); + await testSetWithAllCombination(client); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object encoding test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const string_key = uuidv4(); + const list_key = uuidv4(); + const hashtable_key = uuidv4(); + const intset_key = uuidv4(); + const set_listpack_key = uuidv4(); + const hash_hashtable_key = uuidv4(); + const hash_listpack_key = uuidv4(); + const skiplist_key = uuidv4(); + const zset_listpack_key = uuidv4(); + const stream_key = uuidv4(); + const non_existing_key = uuidv4(); + const versionLessThan7 = + await checkIfServerVersionLessThan("7.0.0"); + const versionLessThan72 = + await checkIfServerVersionLessThan("7.2.0"); + + expect(await client.objectEncoding(non_existing_key)).toEqual( + null, + ); + + checkSimple( + await client.set( + string_key, + "a really loooooooooooooooooooooooooooooooooooooooong value", + ), + ).toEqual("OK"); + + checkSimple(await client.objectEncoding(string_key)).toEqual( + "raw", + ); + + checkSimple(await client.set(string_key, "2")).toEqual("OK"); + checkSimple(await client.objectEncoding(string_key)).toEqual( + "int", + ); + + checkSimple(await client.set(string_key, "value")).toEqual( + "OK", + ); + checkSimple(await client.objectEncoding(string_key)).toEqual( + "embstr", + ); + + expect(await client.lpush(list_key, ["1"])).toEqual(1); + + if (versionLessThan72) { + checkSimple(await client.objectEncoding(list_key)).toEqual( + "quicklist", + ); + } else { + checkSimple(await client.objectEncoding(list_key)).toEqual( + "listpack", + ); + } + + // The default value of set-max-intset-entries is 512 + for (let i = 0; i < 513; i++) { + expect( + await client.sadd(hashtable_key, [String(i)]), + ).toEqual(1); + } + + checkSimple(await client.objectEncoding(hashtable_key)).toEqual( + "hashtable", + ); + + expect(await client.sadd(intset_key, ["1"])).toEqual(1); + checkSimple(await client.objectEncoding(intset_key)).toEqual( + "intset", + ); + + expect(await client.sadd(set_listpack_key, ["foo"])).toEqual(1); + + if (versionLessThan72) { + checkSimple( + await client.objectEncoding(set_listpack_key), + ).toEqual("hashtable"); + } else { + checkSimple( + await client.objectEncoding(set_listpack_key), + ).toEqual("listpack"); + } + + // The default value of hash-max-listpack-entries is 512 + for (let i = 0; i < 513; i++) { + expect( + await client.hset(hash_hashtable_key, { + [String(i)]: "2", + }), + ).toEqual(1); + } + + checkSimple( + await client.objectEncoding(hash_hashtable_key), + ).toEqual("hashtable"); + + expect( + await client.hset(hash_listpack_key, { "1": "2" }), + ).toEqual(1); + + if (versionLessThan7) { + checkSimple( + await client.objectEncoding(hash_listpack_key), + ).toEqual("ziplist"); + } else { + checkSimple( + await client.objectEncoding(hash_listpack_key), + ).toEqual("listpack"); + } + + // The default value of zset-max-listpack-entries is 128 + for (let i = 0; i < 129; i++) { + expect( + await client.zadd(skiplist_key, { [String(i)]: 2.0 }), + ).toEqual(1); + } + + checkSimple(await client.objectEncoding(skiplist_key)).toEqual( + "skiplist", + ); + + expect( + await client.zadd(zset_listpack_key, { "1": 2.0 }), + ).toEqual(1); + + if (versionLessThan7) { + checkSimple( + await client.objectEncoding(zset_listpack_key), + ).toEqual("ziplist"); + } else { + checkSimple( + await client.objectEncoding(zset_listpack_key), + ).toEqual("listpack"); + } + + expect( + await client.xadd(stream_key, [["field", "value"]]), + ).not.toBeNull(); + checkSimple(await client.objectEncoding(stream_key)).toEqual( + "stream", + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object freq test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonExistingKey = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: "allkeys-lfu", + }), + ).toEqual("OK"); + expect(await client.objectFreq(nonExistingKey)).toEqual( + null, + ); + expect(await client.set(key, "foobar")).toEqual("OK"); + expect(await client.objectFreq(key)).toBeGreaterThanOrEqual( + 0, + ); + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object idletime test_%p", + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + const nonExistingKey = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + expect( + await client.configSet({ + // OBJECT IDLETIME requires a non-LFU maxmemory-policy + [maxmemoryPolicyKey]: "allkeys-random", + }), + ).toEqual("OK"); + expect(await client.objectIdletime(nonExistingKey)).toEqual( + null, + ); + expect(await client.set(key, "foobar")).toEqual("OK"); + + await wait(2000); + + expect(await client.objectIdletime(key)).toBeGreaterThan(0); + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + }, protocol); + }, + config.timeout, + ); + + function wait(numMilliseconds: number) { + return new Promise((resolve) => { + setTimeout(resolve, numMilliseconds); + }); + } + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `object refcount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = `{key}:${uuidv4()}`; + const nonExistingKey = `{key}:${uuidv4()}`; + + expect(await client.objectRefcount(nonExistingKey)).toBeNull(); + expect(await client.set(key, "foo")).toEqual("OK"); + expect(await client.objectRefcount(key)).toBeGreaterThanOrEqual( + 1, + ); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `flushall test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + // Test FLUSHALL SYNC + expect(await client.flushall(FlushMode.SYNC)).toBe("OK"); + + // TODO: replace with KEYS command when implemented + const keysAfter = (await client.customCommand([ + "keys", + "*", + ])) as string[]; + expect(keysAfter.length).toBe(0); + + // Test various FLUSHALL calls + expect(await client.flushall()).toBe("OK"); + expect(await client.flushall(FlushMode.ASYNC)).toBe("OK"); + + if (client instanceof GlideClusterClient) { + const key = uuidv4(); + const primaryRoute: SingleNodeRoute = { + type: "primarySlotKey", + key: key, + }; + expect(await client.flushall(undefined, primaryRoute)).toBe( + "OK", + ); + expect( + await client.flushall(FlushMode.ASYNC, primaryRoute), + ).toBe("OK"); + + //Test FLUSHALL on replica (should fail) + const key2 = uuidv4(); + const replicaRoute: SingleNodeRoute = { + type: "replicaSlotKey", + key: key2, + }; + await expect( + client.flushall(undefined, replicaRoute), + ).rejects.toThrowError(); + } + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lpos test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = `{key}:${uuidv4()}`; + const valueArray = ["a", "a", "b", "c", "a", "b"]; + expect(await client.rpush(key, valueArray)).toEqual(6); + + // simplest case + expect(await client.lpos(key, "a")).toEqual(0); + expect( + await client.lpos(key, "b", new LPosOptions({ rank: 2 })), + ).toEqual(5); + + // element doesn't exist + expect(await client.lpos(key, "e")).toBeNull(); + + // reverse traversal + expect( + await client.lpos(key, "b", new LPosOptions({ rank: -2 })), + ).toEqual(2); + + // unlimited comparisons + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 1, maxLength: 0 }), + ), + ).toEqual(0); + + // limited comparisons + expect( + await client.lpos( + key, + "c", + new LPosOptions({ rank: 1, maxLength: 2 }), + ), + ).toBeNull(); + + // invalid rank value + await expect( + client.lpos(key, "a", new LPosOptions({ rank: 0 })), + ).rejects.toThrow(RequestError); + + // invalid maxlen value + await expect( + client.lpos(key, "a", new LPosOptions({ maxLength: -1 })), + ).rejects.toThrow(RequestError); + + // non-existent key + expect(await client.lpos("non-existent_key", "e")).toBeNull(); + + // wrong key data type + const wrongDataType = `{key}:${uuidv4()}`; + expect(await client.sadd(wrongDataType, ["a", "b"])).toEqual(2); + + await expect(client.lpos(wrongDataType, "a")).rejects.toThrow( + RequestError, + ); + + // invalid count value + await expect( + client.lpos(key, "a", new LPosOptions({ count: -1 })), + ).rejects.toThrow(RequestError); + + // with count + expect( + await client.lpos(key, "a", new LPosOptions({ count: 2 })), + ).toEqual([0, 1]); + expect( + await client.lpos(key, "a", new LPosOptions({ count: 0 })), + ).toEqual([0, 1, 4]); + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 1, count: 0 }), + ), + ).toEqual([0, 1, 4]); + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 2, count: 0 }), + ), + ).toEqual([1, 4]); + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: 3, count: 0 }), + ), + ).toEqual([4]); + + // reverse traversal + expect( + await client.lpos( + key, + "a", + new LPosOptions({ rank: -1, count: 0 }), + ), + ).toEqual([4, 1, 0]); + }, protocol); + }, + config.timeout, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `dbsize test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + // flush all data + expect(await client.flushall()).toBe("OK"); + + // check that DBSize is 0 + expect(await client.dbsize()).toBe(0); + + // set 10 random key-value pairs + for (let i = 0; i < 10; i++) { + const key = `{key}:${uuidv4()}`; + const value = "0".repeat(Math.random() * 7); + + expect(await client.set(key, value)).toBe("OK"); + } + + // check DBSIZE after setting + expect(await client.dbsize()).toBe(10); + + // additional test for the standalone client + if (client instanceof GlideClient) { + expect(await client.flushall()).toBe("OK"); + const key = uuidv4(); + expect(await client.set(key, "value")).toBe("OK"); + expect(await client.dbsize()).toBe(1); + // switching to another db to check size + expect(await client.select(1)).toBe("OK"); + expect(await client.dbsize()).toBe(0); + } + + // additional test for the cluster client + if (client instanceof GlideClusterClient) { + expect(await client.flushall()).toBe("OK"); + const key = uuidv4(); + expect(await client.set(key, "value")).toBe("OK"); + const primaryRoute: SingleNodeRoute = { + type: "primarySlotKey", + key: key, + }; + expect(await client.dbsize(primaryRoute)).toBe(1); + } }, protocol); }, config.timeout, @@ -2320,7 +4055,7 @@ export function runCommonTests(config: { const value = "שלום hello 汉字"; await client.set(key, value); const result = await client.get(key); - expect(result).toEqual(value); + checkSimple(result).toEqual(value); }); }, config.timeout, @@ -2332,7 +4067,7 @@ export function runCommonTests(config: { await runTest(async (client: Client) => { const result = await client.get(uuidv4()); - expect(result).toEqual(null); + checkSimple(result).toEqual(null); }); }, config.timeout, @@ -2346,7 +4081,7 @@ export function runCommonTests(config: { await client.set(key, ""); const result = await client.get(key); - expect(result).toEqual(""); + checkSimple(result).toEqual(""); }); }, config.timeout, @@ -2373,7 +4108,7 @@ export function runCommonTests(config: { await client.set(key, value); const result = await client.get(key); - expect(result).toEqual(value); + checkSimple(result).toEqual(value); }); }, config.timeout, @@ -2388,7 +4123,7 @@ export function runCommonTests(config: { await GetAndSetRandomValue(client); } else { const result = await client.get(uuidv4()); - expect(result).toEqual(null); + checkSimple(result).toEqual(null); } }; diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 7288ee541f..d66ede6145 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -1,17 +1,119 @@ /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ import { beforeAll, expect } from "@jest/globals"; import { exec } from "child_process"; +import parseArgs from "minimist"; import { v4 as uuidv4 } from "uuid"; -import { ClusterTransaction, Logger, ReturnType, Transaction } from ".."; +import { + BaseClient, + BaseClientConfiguration, + ClusterTransaction, + GlideClient, + GlideClusterClient, + InsertPosition, + Logger, + ProtocolVersion, + ReturnType, + Transaction, +} from ".."; import { checkIfServerVersionLessThan } from "./SharedTests"; +import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; beforeAll(() => { Logger.init("info"); }); +/* eslint-disable @typescript-eslint/no-explicit-any */ +function intoArrayInternal(obj: any, builder: Array) { + if (obj == null) { + builder.push("null"); + } else if (typeof obj === "string") { + builder.push(obj); + } else if (obj instanceof Uint8Array) { + builder.push(obj.toString()); + } else if (obj instanceof Array) { + for (const item of obj) { + intoArrayInternal(item, builder); + } + } else if (obj instanceof Set) { + const arr = Array.from(obj); + arr.sort(); + + for (const item of arr) { + intoArrayInternal(item, builder); + } + } else if (obj instanceof Map) { + const sortedArr = Array.from(obj.entries()).sort(); + + for (const [key, value] of sortedArr) { + intoArrayInternal(key, builder); + intoArrayInternal(value, builder); + } + } else if (typeof obj[Symbol.iterator] === "function") { + // iterable, recurse into children + for (const item of obj) { + intoArrayInternal(item, builder); + } + } else { + const sortedArr = Array.from(Object.entries(obj)).sort(); + + for (const [k, v] of sortedArr) { + intoArrayInternal(k, builder); + intoArrayInternal(v, builder); + } + } +} + +/** + * accept any variable `v` and convert it into String, recursively + */ +export function intoString(v: any): string { + const builder: Array = []; + intoArrayInternal(v, builder); + return builder.join(""); +} + +/** + * accept any variable `v` and convert it into array of string + */ +export function intoArray(v: any): Array { + const result: Array = []; + intoArrayInternal(v, result); + return result; +} + +/** + * Convert array of strings into array of `Uint8Array` + */ +export function convertStringArrayToBuffer(value: string[]): Uint8Array[] { + const bytesarr: Uint8Array[] = []; + + for (const str of value) { + bytesarr.push(Buffer.from(str)); + } + + return bytesarr; +} + +export class Checker { + left: string; + + constructor(left: any) { + this.left = intoString(left); + } + + toEqual(right: any) { + right = intoString(right); + return expect(this.left).toEqual(right); + } +} + +export function checkSimple(left: any): Checker { + return new Checker(left); +} + export type Client = { set: (key: string, value: string) => Promise; get: (key: string) => Promise; @@ -22,9 +124,9 @@ export async function GetAndSetRandomValue(client: Client) { // Adding random repetition, to prevent the inputs from always having the same alignment. const value = uuidv4() + "0".repeat(Math.random() * 7); const setResult = await client.set(key, value); - expect(setResult).toEqual("OK"); + expect(intoString(setResult)).toEqual("OK"); const result = await client.get(key); - expect(result).toEqual(value); + expect(intoString(result)).toEqual(value); } export function flushallOnPort(port: number): Promise { @@ -40,6 +142,55 @@ export function flushallOnPort(port: number): Promise { ); } +/** + * Parses a string of endpoints into an array of host-port pairs. + * Each endpoint in the string should be in the format 'host:port', separated by commas. + * + * @param endpointsStr - The string containing endpoints in the format 'host:port', separated by commas. + * @returns An array of host-port pairs parsed from the endpoints string. + * @throws If the endpoints string or its format is invalid. + * @example + * ```typescript + * const endpointsStr = 'localhost:8080,example.com:3000,192.168.1.100:5000'; + * const endpoints = parseEndpoints(endpointsStr); + * console.log(endpoints); + * // Output: [['localhost', 8080], ['example.com', 3000], ['192.168.1.100', 5000]] + * ``` + */ +export const parseEndpoints = (endpointsStr: string): [string, number][] => { + try { + console.log(endpointsStr); + const endpoints: string[][] = endpointsStr + .split(",") + .map((endpoint) => endpoint.split(":")); + endpoints.forEach((endpoint) => { + // Check if each endpoint has exactly two elements (host and port) + if (endpoint.length !== 2) { + throw new Error( + "Each endpoint should be in the format 'host:port'.\nEndpoints should be separated by commas.", + ); + } + + // Extract host and port + const [host, portStr] = endpoint; + + // Check if both host and port are specified and if port is a valid integer + if (!host || !portStr || !/^\d+$/.test(portStr)) { + throw new Error( + "Both host and port should be specified and port should be a valid integer.", + ); + } + }); + + // Convert port strings to numbers and return the result + return endpoints.map(([host, portStr]) => [host, Number(portStr)]); + } catch (error) { + throw new Error( + "Invalid endpoints format: " + (error as Error).message, + ); + } +}; + /// This function takes the first result of the response if it got more than one response (like cluster responses). export function getFirstResult( res: string | number | Record | Record, @@ -51,6 +202,97 @@ export function getFirstResult( return Object.values(res).at(0); } +/** + * Parses the command-line arguments passed to the Node.js process. + * + * @returns Parsed command-line arguments. + * + * @example + * ```typescript + * // Command: node script.js --name="John Doe" --age=30 + * const args = parseCommandLineArgs(); + * // args = { name: 'John Doe', age: 30 } + * ``` + */ +export function parseCommandLineArgs() { + return parseArgs(process.argv.slice(2)); +} + +export async function testTeardown( + cluster_mode: boolean, + option: BaseClientConfiguration, +) { + const client = cluster_mode + ? await GlideClusterClient.createClient(option) + : await GlideClient.createClient(option); + + await client.customCommand(["FLUSHALL"]); + client.close(); +} + +export const getClientConfigurationOption = ( + addresses: [string, number][], + protocol: ProtocolVersion, + timeout?: number, +): BaseClientConfiguration => { + return { + addresses: addresses.map(([host, port]) => ({ + host, + port, + })), + protocol, + ...(timeout && { requestTimeout: timeout }), + }; +}; + +export async function flushAndCloseClient( + cluster_mode: boolean, + addresses: [string, number][], + client?: BaseClient, +) { + await testTeardown( + cluster_mode, + getClientConfigurationOption(addresses, ProtocolVersion.RESP3, 2000), + ); + + // some tests don't initialize a client + if (client == undefined) { + return; + } + + client.close(); +} + +/** + * Compare two maps by converting them to JSON strings and checking for equality, including property order. + * + * @param map - The first map to compare. + * @param map2 - The second map to compare. + * @returns True if the maps are equal. + * @remarks This function is used to compare maps, including their property order. + * Direct comparison with `expect(map).toEqual(map2)` might ignore the order of properties, + * whereas this function considers property order in the comparison by converting the maps to JSON strings. + * This ensures a stricter comparison that takes property order into account. + * + * @example + * ```typescript + * const mapA = { name: 'John', age: 30 }; + * const mapB = { age: 30, name: 'John' }; + * + * // Direct comparison will pass because it ignores property order + * expect(mapA).toEqual(mapB); // This will pass + * + * // Correct comparison using compareMaps function + * compareMaps(mapA, mapB); // This will return false due to different property order + * ``` + */ +export function compareMaps( + map: Record, + map2: Record, +): boolean { + return JSON.stringify(map) == JSON.stringify(map2); +} + export async function transactionTest( baseTransaction: Transaction | ClusterTransaction, ): Promise { @@ -64,11 +306,23 @@ export async function transactionTest( const key8 = "{key}" + uuidv4(); const key9 = "{key}" + uuidv4(); const key10 = "{key}" + uuidv4(); + const key11 = "{key}" + uuidv4(); // hyper log log + const key12 = "{key}" + uuidv4(); + const key13 = "{key}" + uuidv4(); + const key14 = "{key}" + uuidv4(); // sorted set + const key15 = "{key}" + uuidv4(); // list + const key16 = "{key}" + uuidv4(); // list const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; + baseTransaction.flushall(); + args.push("OK"); + baseTransaction.dbsize(); + args.push(0); baseTransaction.set(key1, "bar"); args.push("OK"); + baseTransaction.objectEncoding(key1); + args.push("embstr"); baseTransaction.type(key1); args.push("string"); baseTransaction.echo(value); @@ -122,10 +376,19 @@ export async function transactionTest( args.push(1); baseTransaction.ltrim(key5, 0, 1); args.push("OK"); + baseTransaction.lset(key5, 0, field + "3"); + args.push("OK"); baseTransaction.lrange(key5, 0, -1); args.push([field + "3", field + "2"]); baseTransaction.lpopCount(key5, 2); args.push([field + "3", field + "2"]); + baseTransaction.linsert( + key5, + InsertPosition.Before, + "nonExistingPivot", + "element", + ); + args.push(0); baseTransaction.rpush(key6, [field + "1", field + "2", field + "3"]); args.push(3); baseTransaction.lindex(key6, 0); @@ -134,18 +397,68 @@ export async function transactionTest( args.push(field + "3"); baseTransaction.rpopCount(key6, 2); args.push([field + "2", field + "1"]); + baseTransaction.rpushx(key15, ["_"]); // key15 is empty + args.push(0); + baseTransaction.lpushx(key15, ["_"]); + args.push(0); + baseTransaction.rpush(key16, [ + field + "1", + field + "1", + field + "2", + field + "3", + field + "3", + ]); + args.push(5); + baseTransaction.lpos(key16, field + "1", new LPosOptions({ rank: 2 })); + args.push(1); + baseTransaction.lpos( + key16, + field + "1", + new LPosOptions({ rank: 2, count: 0 }), + ); + args.push([1]); baseTransaction.sadd(key7, ["bar", "foo"]); args.push(2); + baseTransaction.sunionstore(key7, [key7, key7]); + args.push(2); + baseTransaction.sunion([key7, key7]); + args.push(new Set(["bar", "foo"])); + baseTransaction.sinter([key7, key7]); + args.push(new Set(["bar", "foo"])); + + if (!(await checkIfServerVersionLessThan("7.0.0"))) { + baseTransaction.sintercard([key7, key7]); + args.push(2); + baseTransaction.sintercard([key7, key7], 1); + args.push(1); + } + + baseTransaction.sinterstore(key7, [key7, key7]); + args.push(2); + baseTransaction.sdiff([key7, key7]); + args.push(new Set()); + baseTransaction.sdiffstore(key7, [key7]); + args.push(2); baseTransaction.srem(key7, ["foo"]); args.push(1); baseTransaction.scard(key7); args.push(1); baseTransaction.sismember(key7, "bar"); args.push(true); + + if (!(await checkIfServerVersionLessThan("6.2.0"))) { + baseTransaction.smismember(key7, ["bar", "foo", "baz"]); + args.push([true, true, false]); + } + baseTransaction.smembers(key7); - args.push(["bar"]); + args.push(new Set(["bar"])); baseTransaction.spop(key7); args.push("bar"); + baseTransaction.spopCount(key7, 2); + args.push(new Set()); + baseTransaction.smove(key7, key7, "non_existing_member"); + args.push(false); baseTransaction.scard(key7); args.push(0); baseTransaction.zadd(key8, { @@ -176,6 +489,12 @@ export async function transactionTest( args.push(["member2", "member3", "member4", "member5"]); baseTransaction.zrangeWithScores(key8, { start: 0, stop: -1 }); args.push({ member2: 3, member3: 3.5, member4: 4, member5: 5 }); + baseTransaction.zadd(key12, { one: 1, two: 2 }); + args.push(2); + baseTransaction.zadd(key13, { one: 1, two: 2, tree: 3.5 }); + args.push(3); + baseTransaction.zinterstore(key12, [key12, key13]); + args.push(2); baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); args.push(4); baseTransaction.zpopmin(key8); @@ -189,19 +508,31 @@ export async function transactionTest( "negativeInfinity", "positiveInfinity", ); - args.push(1); + args.push(1); // key8 is now empty + + if (!(await checkIfServerVersionLessThan("7.0.0"))) { + baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); + args.push(2); + baseTransaction.zintercard([key8, key14]); + args.push(0); + baseTransaction.zintercard([key8, key14], 1); + args.push(0); + } + baseTransaction.xadd(key9, [["field", "value1"]], { id: "0-1" }); args.push("0-1"); baseTransaction.xadd(key9, [["field", "value2"]], { id: "0-2" }); args.push("0-2"); baseTransaction.xadd(key9, [["field", "value3"]], { id: "0-3" }); args.push("0-3"); + baseTransaction.xlen(key9); + args.push(3); baseTransaction.xread({ [key9]: "0-1" }); args.push({ - [key9]: [ - ["0-2", ["field", "value2"]], - ["0-3", ["field", "value3"]], - ], + [key9]: { + "0-2": [["field", "value2"]], + "0-3": [["field", "value3"]], + }, }); baseTransaction.xtrim(key9, { method: "minid", @@ -213,102 +544,19 @@ export async function transactionTest( args.push("OK"); baseTransaction.exists([key10]); args.push(1); + baseTransaction.renamenx(key10, key9); + args.push(true); + baseTransaction.exists([key9, key10]); + args.push(1); baseTransaction.rpush(key6, [field + "1", field + "2", field + "3"]); args.push(3); baseTransaction.brpop([key6], 0.1); args.push([key6, field + "3"]); + baseTransaction.blpop([key6], 0.1); + args.push([key6, field + "1"]); + baseTransaction.pfadd(key11, ["a", "b", "c"]); + args.push(1); + baseTransaction.pfcount([key11]); + args.push(3); return args; } - -export class RedisCluster { - private usedPorts: number[]; - private clusterFolder: string; - - private constructor(ports: number[], clusterFolder: string) { - this.usedPorts = ports; - this.clusterFolder = clusterFolder; - } - - private static parseOutput(input: string): { - clusterFolder: string; - ports: number[]; - } { - const lines = input.split(/\r\n|\r|\n/); - const clusterFolder = lines - .find((line) => line.startsWith("CLUSTER_FOLDER")) - ?.split("=")[1]; - const ports = lines - .find((line) => line.startsWith("CLUSTER_NODES")) - ?.split("=")[1] - .split(",") - .map((address) => address.split(":")[1]) - .map((port) => Number(port)); - - if (clusterFolder === undefined || ports === undefined) { - throw new Error(`Insufficient data in input: ${input}`); - } - - return { - clusterFolder, - ports, - }; - } - - public static createCluster( - cluster_mode: boolean, - shardCount: number, - replicaCount: number, - loadModule?: string[], - ): Promise { - return new Promise((resolve, reject) => { - let command = `python3 ../utils/cluster_manager.py start -r ${replicaCount} -n ${shardCount}`; - - if (cluster_mode) { - command += " --cluster-mode"; - } - - if (loadModule) { - if (loadModule.length === 0) { - throw new Error( - "Please provide the path(s) to the module(s) you want to load.", - ); - } - - for (const module of loadModule) { - command += ` --load-module ${module}`; - } - } - - console.log(command); - exec(command, (error, stdout, stderr) => { - if (error) { - console.error(stderr); - reject(error); - } else { - const { clusterFolder, ports } = this.parseOutput(stdout); - resolve(new RedisCluster(ports, clusterFolder)); - } - }); - }); - } - - public ports(): number[] { - return [...this.usedPorts]; - } - - public async close() { - await new Promise((resolve, reject) => - exec( - `python3 ../utils/cluster_manager.py stop --cluster-folder ${this.clusterFolder}`, - (error, _, stderr) => { - if (error) { - console.error(stderr); - reject(error); - } else { - resolve(); - } - }, - ), - ); - } -} diff --git a/node/tests/UtilsTests.test.ts b/node/tests/UtilsTests.test.ts new file mode 100644 index 0000000000..2cc267812e --- /dev/null +++ b/node/tests/UtilsTests.test.ts @@ -0,0 +1,104 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +import { describe, expect, it } from "@jest/globals"; +import { compareMaps } from "./TestUtilities"; + +describe("test compareMaps", () => { + it("Map comparison of empty maps should return true", () => { + const map1 = {}; + const map2 = {}; + expect(compareMaps(map1, map2)).toBe(true); + }); + + it("Map comparison of maps with the same key-value pairs should return true", () => { + const map1 = { a: 1, b: 2 }; + const map2 = { a: 1, b: 2 }; + expect(compareMaps(map1, map2)).toBe(true); + }); + + it("Map comparison of maps with different key-value pairs should return false", () => { + const map1 = { a: 1, b: 2 }; + const map2 = { a: 1, b: 3 }; + expect(compareMaps(map1, map2)).toBe(false); + }); + + it("Map comparison of maps with different key-value pairs order should return false", () => { + const map1 = { a: 1, b: 2 }; + const map2 = { b: 2, a: 1 }; + expect(compareMaps(map1, map2)).toBe(false); + }); + + it("Map comparison of maps with nested maps having the same values should return true", () => { + const map1 = { a: { b: 1 } }; + const map2 = { a: { b: 1 } }; + expect(compareMaps(map1, map2)).toBe(true); + }); + + it("Map comparison of maps with nested maps having different values should return false", () => { + const map1 = { a: { b: 1 } }; + const map2 = { a: { b: 2 } }; + expect(compareMaps(map1, map2)).toBe(false); + }); + + it("Map comparison of maps with nested maps having different order should return false", () => { + const map1 = { a: { b: 1, c: 2 } }; + const map2 = { a: { c: 2, b: 1 } }; + expect(compareMaps(map1, map2)).toBe(false); + }); + + it("Map comparison of maps with arrays as values having the same values should return true", () => { + const map1 = { a: [1, 2] }; + const map2 = { a: [1, 2] }; + expect(compareMaps(map1, map2)).toBe(true); + }); + + it("Map comparison of maps with arrays as values having different values should return false", () => { + const map1 = { a: [1, 2] }; + const map2 = { a: [1, 3] }; + expect(compareMaps(map1, map2)).toBe(false); + }); + + it("Map comparison of maps with null values should return true", () => { + const map1 = { a: null }; + const map2 = { a: null }; + expect(compareMaps(map1, map2)).toBe(true); + }); + + it("Map comparison of maps with mixed types of values should return true", () => { + const map1 = { + a: 1, + b: { c: [2, 3] }, + d: null, + e: "string", + f: [1, "2", true], + }; + const map2 = { + a: 1, + b: { c: [2, 3] }, + d: null, + e: "string", + f: [1, "2", true], + }; + expect(compareMaps(map1, map2)).toBe(true); + }); + + it("Map comparison of maps with mixed types of values should return false", () => { + const map1 = { + a: 1, + b: { c: [2, 3] }, + d: null, + e: "string", + f: [1, "2", false], + }; + const map2 = { + a: 1, + b: { c: [2, 3] }, + d: null, + f: [1, "2", false], + e: "string", + }; + expect(compareMaps(map1, map2)).toBe(false); + }); +}); diff --git a/python/Cargo.toml b/python/Cargo.toml index 842526ed8d..16632945bb 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "glide-for-redis" +name = "valkey-glide" version = "255.255.255" edition = "2021" license = "Apache-2.0" -authors = ["Amazon Web Services"] +authors = ["Valkey GLIDE Maintainers"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -12,6 +12,7 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "^0.20", features = ["extension-module", "num-bigint"] } +bytes = { version = "1.6.0" } redis = { path = "../submodules/redis-rs/redis", features = ["aio", "tokio-comp", "connection-manager","tokio-rustls-comp"] } glide-core = { path = "../glide-core", features = ["socket-layer"] } logger_core = {path = "../logger_core"} diff --git a/python/DEVELOPER.md b/python/DEVELOPER.md index 3368616b69..ac9a6555c5 100644 --- a/python/DEVELOPER.md +++ b/python/DEVELOPER.md @@ -1,10 +1,10 @@ # Developer Guide -This document describes how to set up your development environment to build and test the GLIDE for Redis Python wrapper. +This document describes how to set up your development environment to build and test the Valkey GLIDE Python wrapper. ### Development Overview -The GLIDE for Redis Python wrapper consists of both Python and Rust code. Rust bindings for Python are implemented using [PyO3](https://github.com/PyO3/pyo3), and the Python package is built using [maturin](https://github.com/PyO3/maturin). The Python and Rust components communicate using the [protobuf](https://github.com/protocolbuffers/protobuf) protocol. +The Valkey GLIDE Python wrapper consists of both Python and Rust code. Rust bindings for Python are implemented using [PyO3](https://github.com/PyO3/pyo3), and the Python package is built using [maturin](https://github.com/PyO3/maturin). The Python and Rust components communicate using the [protobuf](https://github.com/protocolbuffers/protobuf) protocol. ### Build from source @@ -33,6 +33,8 @@ source "$HOME/.cargo/env" rustc --version # Install protobuf compiler PB_REL="https://github.com/protocolbuffers/protobuf/releases" +# For other arch type from x86 example below, the signature of the curl url should be protoc---.zip, +# e.g. protoc-3.20.3-linux-aarch_64.zip for ARM64. curl -LO $PB_REL/download/v3.20.3/protoc-3.20.3-linux-x86_64.zip unzip protoc-3.20.3-linux-x86_64.zip -d $HOME/.local export PATH="$PATH:$HOME/.local/bin" @@ -64,12 +66,18 @@ protoc --version ```bash brew update -brew install python3 git gcc pkgconfig protobuf@3 openssl -pip3 install virtualenv +brew install python3 git gcc pkgconfig protobuf@3 openssl virtualenv curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" # Check that the Rust compiler is installed rustc --version +# Verify the Protobuf compiler installation +protoc --version + +# If protoc is not found or does not work correctly, update the PATH +echo 'export PATH="/opt/homebrew/opt/protobuf@3/bin:$PATH"' >> /Users/$USER/.bash_profile +source /Users/$USER/.bash_profile +protoc --version ``` #### Building and installation steps @@ -79,7 +87,7 @@ Before starting this step, make sure you've installed all software requirments. 1. Clone the repository: ```bash VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch - git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git + git clone --branch ${VERSION} https://github.com/valkey-io/valkey-glide.git cd glide-for-redis ``` 2. Initialize git submodule: @@ -116,7 +124,7 @@ Before starting this step, make sure you've installed all software requirments. ```bash pytest --asyncio-mode=auto ``` - > **Note:** To run redis modules tests, add -k "test_redis_modules.py". + > **Note:** To run redis modules tests, add -k "test_server_modules.py". - Install Python development requirements with: @@ -153,7 +161,7 @@ git submodule update During the initial build, Python protobuf files were created in `python/python/glide/protobuf`. If modifications are made to the protobuf definition files (.proto files located in `glide-core/src/protofuf`), it becomes necessary to regenerate the Python protobuf files. To do so, run: ```bash -GLIDE_ROOT_FOLDER_PATH=. # e.g. /home/ubuntu/glide-for-redis +GLIDE_ROOT_FOLDER_PATH=. # e.g. /home/ubuntu/valkey-glide protoc -Iprotobuf=${GLIDE_ROOT_FOLDER_PATH}/glide-core/src/protobuf/ --python_out=${GLIDE_ROOT_FOLDER_PATH}/python/python/glide ${GLIDE_ROOT_FOLDER_PATH}/glide-core/src/protobuf/*.proto ``` @@ -162,7 +170,7 @@ protoc -Iprotobuf=${GLIDE_ROOT_FOLDER_PATH}/glide-core/src/protobuf/ --python_ou To generate the protobuf files with Python Interface files (pyi) for type-checking purposes, ensure you have installed `mypy-protobuf` with pip, and then execute the following command: ```bash -GLIDE_ROOT_FOLDER_PATH=. # e.g. /home/ubuntu/glide-for-redis +GLIDE_ROOT_FOLDER_PATH=. # e.g. /home/ubuntu/valkey-glide MYPY_PROTOC_PATH=`which protoc-gen-mypy` protoc --plugin=protoc-gen-mypy=${MYPY_PROTOC_PATH} -Iprotobuf=${GLIDE_ROOT_FOLDER_PATH}/glide-core/src/protobuf/ --python_out=${GLIDE_ROOT_FOLDER_PATH}/python/python/glide --mypy_out=${GLIDE_ROOT_FOLDER_PATH}/python/python/glide ${GLIDE_ROOT_FOLDER_PATH}/glide-core/src/protobuf/*.proto ``` @@ -193,8 +201,8 @@ Run from the main `/python` folder > Note: make sure to [generate protobuf with interface files]("#protobuf-interface-files") before running mypy linter ```bash pip install -r dev_requirements.txt - isort . --profile black --skip-glob python/glide/protobuf - black . --exclude python/glide/protobuf + isort . --profile black --skip-glob python/glide/protobuf --skip-glob .env + black . --exclude python/glide/protobuf --exclude .env flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=python/glide/protobuf,.env/* --extend-ignore=E230 flake8 . --count --exit-zero --max-complexity=12 --max-line-length=127 --statistics --exclude=python/glide/protobuf,.env/* --extend-ignore=E230 # run type check diff --git a/python/README.md b/python/README.md index 30353a7c31..89c4bec560 100644 --- a/python/README.md +++ b/python/README.md @@ -1,37 +1,37 @@ -# GLIDE for Redis +# Valkey GLIDE -General Language Independent Driver for the Enterprise (GLIDE) for Redis, is an AWS-sponsored, open-source Redis client. GLIDE for Redis works with any Redis distribution that adheres to the Redis Serialization Protocol (RESP) specification, including open-source Redis, Amazon ElastiCache for Redis, and Amazon MemoryDB for Redis. -Strategic, mission-critical Redis-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Redis is designed to provide a client experience that helps meet these objectives. It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Redis-compatible services used by hundreds of thousands of customers. To help ensure consistency in development and operations, GLIDE for Redis is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. In this Preview release, GLIDE for Redis is available for Python and Javascript (Node.js), with support for Java actively under development. +Valkey General Language Independent Driver for the Enterprise (GLIDE), is an open-source Valkey client library. Valkey GLIDE is one of the official client libraries for Valkey, and it supports all Valkey commands. Valkey GLIDE supports Valkey 7.2 and above, and Redis open-source 6.2, 7.0 and 7.2. Application programmers use Valkey GLIDE to safely and reliably connect their applications to Valkey- and Redis OSS- compatible services. Valkey GLIDE is designed for reliability, optimized performance, and high-availability, for Valkey and Redis OSS based applications. It is sponsored and supported by AWS, and is pre-configured with best practices learned from over a decade of operating Redis OSS-compatible services used by hundreds of thousands of customers. To help ensure consistency in application development and operations, Valkey GLIDE is implemented using a core driver framework, written in Rust, with language specific extensions. This design ensures consistency in features across languages and reduces overall complexity. -## Supported Redis Versions +## Supported Engine Versions -GLIDE for Redis is API-compatible with open source Redis version 6 and 7. - -## Current Status - -We've made GLIDE for Redis an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. -This preview release is recommended for testing purposes only. +Refer to the [Supported Engine Versions table](https://github.com/valkey-io/valkey-glide/blob/main/README.md#supported-engine-versions) for details. # Getting Started - Python Wrapper ## System Requirements -The beta release of GLIDE for Redis was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. +The beta release of Valkey GLIDE was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. -## Python supported version +## Python Supported Versions -Python 3.8 or higher. +| Python Version | +|----------------| +| 3.8 | +| 3.9 | +| 3.10 | +| 3.11 | +| 3.12 | ## Installation and Setup ### Installing via Package Manager (pip) -To install GLIDE for Redis using `pip`, follow these steps: +To install Valkey GLIDE using `pip`, follow these steps: 1. Open your terminal. 2. Execute the command below: ```bash - $ pip install glide-for-redis + $ pip install valkey-glide ``` 3. After installation, confirm the client is accessible by running: ```bash @@ -41,15 +41,15 @@ To install GLIDE for Redis using `pip`, follow these steps: ## Basic Examples -#### Cluster Redis: +#### Cluster Mode: ```python: >>> import asyncio ->>> from glide import ClusterClientConfiguration, NodeAddress, RedisClusterClient +>>> from glide import GlideClusterClientConfiguration, NodeAddress, GlideClusterClient >>> async def test_cluster_client(): -... addresses = [NodeAddress("redis.example.com", 6379)] -... config = ClusterClientConfiguration(addresses) -... client = await RedisClusterClient.create(config) +... addresses = [NodeAddress("address.example.com", 6379)] +... config = GlideClusterClientConfiguration(addresses) +... client = await GlideClusterClient.create(config) ... set_result = await client.set("foo", "bar") ... print(f"Set response is {set_result}") ... get_result = await client.get("foo") @@ -60,18 +60,18 @@ Set response is OK Get response is bar ``` -#### Standalone Redis: +#### Standalone Mode: ```python: >>> import asyncio ->>> from glide import RedisClientConfiguration, NodeAddress, RedisClient +>>> from glide import GlideClientConfiguration, NodeAddress, GlideClient >>> async def test_standalone_client(): ... addresses = [ -... NodeAddress("redis_primary.example.com", 6379), -... NodeAddress("redis_replica.example.com", 6379) +... NodeAddress("server_primary.example.com", 6379), +... NodeAddress("server_replica.example.com", 6379) ... ] -... config = RedisClientConfiguration(addresses) -... client = await RedisClient.create(config) +... config = GlideClientConfiguration(addresses) +... client = await GlideClient.create(config) ... set_result = await client.set("foo", "bar") ... print(f"Set response is {set_result}") ... get_result = await client.get("foo") @@ -82,10 +82,12 @@ Set response is OK Get response is bar ``` +For complete examples with error handling, please refer to the [cluster example](https://github.com/valkey-io/valkey-glide/blob/main/examples/python/cluster_example.py) and the [standalone example](https://github.com/valkey-io/valkey-glide/blob/main/examples/python/standalone_example.py). + ## Documentation -Visit our [wiki](https://github.com/aws/glide-for-redis/wiki/Python-wrapper) for examples and further details on TLS, Read strategy, Timeouts and various other configurations. +Visit our [wiki](https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper) for examples and further details on TLS, Read strategy, Timeouts and various other configurations. ### Building & Testing -Development instructions for local building & testing the package are in the [DEVELOPER.md](https://github.com/aws/glide-for-redis/blob/main/python/DEVELOPER.md#build-from-source) file. +Development instructions for local building & testing the package are in the [DEVELOPER.md](https://github.com/valkey-io/valkey-glide/blob/main/python/DEVELOPER.md#build-from-source) file. diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index 412d011290..fe669c485d 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -212,7 +212,7 @@ The applicable license information is listed below: ---- -Package: addr2line:0.21.0 +Package: addr2line:0.22.0 The following copyrights and licenses were found in the source code of this package: @@ -1828,7 +1828,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: arcstr:1.1.5 +Package: arcstr:1.2.0 The following copyrights and licenses were found in the source code of this package: @@ -2077,7 +2077,7 @@ the following restrictions: ---- -Package: async-trait:0.1.80 +Package: async-trait:0.1.81 The following copyrights and licenses were found in the source code of this package: @@ -2535,7 +2535,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: backtrace:0.3.71 +Package: backtrace:0.3.73 The following copyrights and licenses were found in the source code of this package: @@ -2764,7 +2764,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: base64:0.22.0 +Package: base64:0.22.1 The following copyrights and licenses were found in the source code of this package: @@ -2993,7 +2993,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bitflags:1.3.2 +Package: bitflags:2.6.0 The following copyrights and licenses were found in the source code of this package: @@ -3222,7 +3222,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bitflags:2.5.0 +Package: bumpalo:3.16.0 The following copyrights and licenses were found in the source code of this package: @@ -3451,7 +3451,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bumpalo:3.16.0 +Package: bytes:1.6.1 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: cfg-if:1.0.0 The following copyrights and licenses were found in the source code of this package: @@ -3680,10 +3705,214 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: bytes:1.6.0 +Package: chrono:0.4.38 The following copyrights and licenses were found in the source code of this package: + 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 [yyyy] [name of copyright owner] + + 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. + + -- + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -3705,7 +3934,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: cfg-if:1.0.0 +Package: combine:4.6.7 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: core-foundation:0.9.4 The following copyrights and licenses were found in the source code of this package: @@ -3934,7 +4188,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: chrono:0.4.38 +Package: core-foundation-sys:0.8.6 The following copyrights and licenses were found in the source code of this package: @@ -4163,7 +4417,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: combine:4.6.7 +Package: crc16:0.4.0 The following copyrights and licenses were found in the source code of this package: @@ -4188,7 +4442,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation:0.9.4 +Package: crc32fast:1.4.2 The following copyrights and licenses were found in the source code of this package: @@ -4417,7 +4671,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: core-foundation-sys:0.8.6 +Package: crossbeam-channel:0.5.13 The following copyrights and licenses were found in the source code of this package: @@ -4646,32 +4900,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crc16:0.4.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: crc32fast:1.4.0 +Package: crossbeam-utils:0.8.20 The following copyrights and licenses were found in the source code of this package: @@ -4900,7 +5129,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-channel:0.5.12 +Package: deranged:0.3.11 The following copyrights and licenses were found in the source code of this package: @@ -5129,7 +5358,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-utils:0.8.19 +Package: derivative:2.2.0 The following copyrights and licenses were found in the source code of this package: @@ -5358,7 +5587,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: deranged:0.3.11 +Package: directories:4.0.1 The following copyrights and licenses were found in the source code of this package: @@ -5587,7 +5816,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: derivative:2.2.0 +Package: dirs-sys:0.3.7 The following copyrights and licenses were found in the source code of this package: @@ -5816,7 +6045,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: directories:4.0.1 +Package: dispose:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -6045,7 +6274,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dirs-sys:0.3.7 +Package: dispose-derive:0.4.0 The following copyrights and licenses were found in the source code of this package: @@ -6274,7 +6503,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dispose:0.5.0 +Package: fast-math:0.1.1 The following copyrights and licenses were found in the source code of this package: @@ -6503,214 +6732,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: dispose-derive:0.4.0 +Package: file-rotate:0.7.6 The following copyrights and licenses were found in the source code of this package: - 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 [yyyy] [name of copyright owner] - - 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. - - -- - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -6732,7 +6757,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: fast-math:0.1.1 +Package: flate2:1.0.30 The following copyrights and licenses were found in the source code of this package: @@ -6961,32 +6986,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: file-rotate:0.7.5 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: flate2:1.0.28 +Package: form_urlencoded:1.2.1 The following copyrights and licenses were found in the source code of this package: @@ -7215,7 +7215,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: form_urlencoded:1.2.1 +Package: futures:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7444,7 +7444,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures:0.3.30 +Package: futures-channel:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7673,7 +7673,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-channel:0.3.30 +Package: futures-core:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -7902,7 +7902,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-core:0.3.30 +Package: futures-executor:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8131,7 +8131,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-executor:0.3.30 +Package: futures-intrusive:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -8360,7 +8360,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-intrusive:0.5.0 +Package: futures-io:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8589,7 +8589,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-io:0.3.30 +Package: futures-macro:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -8818,7 +8818,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-macro:0.3.30 +Package: futures-sink:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9047,7 +9047,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-sink:0.3.30 +Package: futures-task:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9276,7 +9276,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-task:0.3.30 +Package: futures-util:0.3.30 The following copyrights and licenses were found in the source code of this package: @@ -9505,7 +9505,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: futures-util:0.3.30 +Package: getrandom:0.2.15 The following copyrights and licenses were found in the source code of this package: @@ -9734,7 +9734,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: getrandom:0.2.14 +Package: gimli:0.29.0 The following copyrights and licenses were found in the source code of this package: @@ -9963,7 +9963,215 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: gimli:0.28.1 +Package: glide-core:0.1.0 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + +---- + +Package: hashbrown:0.14.5 The following copyrights and licenses were found in the source code of this package: @@ -10192,7 +10400,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: glide-core:0.1.0 +Package: heck:0.4.1 The following copyrights and licenses were found in the source code of this package: @@ -10398,9 +10606,30 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---- -Package: hashbrown:0.14.3 +Package: heck:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -10629,7 +10858,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: heck:0.4.1 +Package: hermit-abi:0.3.9 The following copyrights and licenses were found in the source code of this package: @@ -10858,7 +11087,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.9 +Package: iana-time-zone:0.1.60 The following copyrights and licenses were found in the source code of this package: @@ -11087,7 +11316,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: iana-time-zone:0.1.60 +Package: iana-time-zone-haiku:0.1.2 The following copyrights and licenses were found in the source code of this package: @@ -11316,7 +11545,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: iana-time-zone-haiku:0.1.2 +Package: idna:0.5.0 The following copyrights and licenses were found in the source code of this package: @@ -11545,7 +11774,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: idna:0.5.0 +Package: ieee754:0.2.6 The following copyrights and licenses were found in the source code of this package: @@ -11774,7 +12003,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ieee754:0.2.6 +Package: indoc:2.0.5 The following copyrights and licenses were found in the source code of this package: @@ -12003,7 +12232,63 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: indoc:2.0.5 +Package: instant:0.1.13 + +The following copyrights and licenses were found in the source code of this package: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the ORGANIZATION nor the names of its contributors may be +used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +Package: integer-encoding:4.0.0 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: itoa:1.0.11 The following copyrights and licenses were found in the source code of this package: @@ -12232,63 +12517,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: instant:0.1.12 - -The following copyrights and licenses were found in the source code of this package: - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of the ORGANIZATION nor the names of its contributors may be -used to endorse or promote products derived from this software without specific -prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF -THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - -Package: integer-encoding:4.0.0 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: itoa:1.0.11 +Package: js-sys:0.3.69 The following copyrights and licenses were found in the source code of this package: @@ -12517,7 +12746,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.69 +Package: lazy_static:1.5.0 The following copyrights and licenses were found in the source code of this package: @@ -12746,7 +12975,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: lazy_static:1.4.0 +Package: libc:0.2.155 The following copyrights and licenses were found in the source code of this package: @@ -12975,7 +13204,32 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libc:0.2.153 +Package: libredox:0.1.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: lock_api:0.4.12 The following copyrights and licenses were found in the source code of this package: @@ -13204,32 +13458,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libredox:0.1.3 - -The following copyrights and licenses were found in the source code of this package: - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ----- - -Package: lock_api:0.4.11 +Package: log:0.4.22 The following copyrights and licenses were found in the source code of this package: @@ -13458,7 +13687,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: log:0.4.21 +Package: logger_core:0.1.0 The following copyrights and licenses were found in the source code of this package: @@ -13664,8 +13893,64 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. +---- + +Package: memchr:2.7.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + -- +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +---- + +Package: memoffset:0.9.1 + +The following copyrights and licenses were found in the source code of this package: + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including @@ -13687,7 +13972,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: logger_core:0.1.0 +Package: miniz_oxide:0.7.4 The following copyrights and licenses were found in the source code of this package: @@ -13893,11 +14178,7 @@ The following copyrights and licenses were found in the source code of this pack See the License for the specific language governing permissions and limitations under the License. ----- - -Package: memchr:2.7.2 - -The following copyrights and licenses were found in the source code of this package: + -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -13920,34 +14201,27 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- -This is free and unencumbered software released into the public domain. +This software is provided 'as-is', without any express or implied warranty. In no +event will the authors be held liable for any damages arising from the use of this +software. -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. +1. The origin of this software must not be misrepresented; you must not claim that + you wrote the original software. If you use this software in a product, an + acknowledgment in the product documentation would be appreciated but is not + required. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. -For more information, please refer to +3. This notice may not be removed or altered from any source distribution. ---- -Package: memoffset:0.9.1 +Package: mio:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -13972,256 +14246,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: miniz_oxide:0.7.2 - -The following copyrights and licenses were found in the source code of this package: - - 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 [yyyy] [name of copyright owner] - - 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. - - -- - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -- - -This software is provided 'as-is', without any express or implied warranty. In no -event will the authors be held liable for any damages arising from the use of this -software. - -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that - you wrote the original software. If you use this software in a product, an - acknowledgment in the product documentation would be appreciated but is not - required. - -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. - ----- - -Package: mio:0.8.11 +Package: nanoid:0.4.0 The following copyrights and licenses were found in the source code of this package: @@ -14271,7 +14296,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-bigint:0.4.4 +Package: num-bigint:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -14958,7 +14983,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: num-traits:0.2.18 +Package: num-traits:0.2.19 The following copyrights and licenses were found in the source code of this package: @@ -15416,7 +15441,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: object:0.32.2 +Package: object:0.36.1 The following copyrights and licenses were found in the source code of this package: @@ -16128,7 +16153,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: parking_lot:0.12.1 +Package: parking_lot:0.12.3 The following copyrights and licenses were found in the source code of this package: @@ -16357,7 +16382,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: parking_lot_core:0.9.9 +Package: parking_lot_core:0.9.10 The following copyrights and licenses were found in the source code of this package: @@ -18876,7 +18901,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: proc-macro2:1.0.81 +Package: proc-macro2:1.0.86 The following copyrights and licenses were found in the source code of this package: @@ -19105,7 +19130,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.4.0 +Package: protobuf:3.5.0 The following copyrights and licenses were found in the source code of this package: @@ -19130,7 +19155,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.4.0 +Package: protobuf-support:3.5.0 The following copyrights and licenses were found in the source code of this package: @@ -21247,7 +21272,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: redox_syscall:0.4.1 +Package: redox_syscall:0.5.3 The following copyrights and licenses were found in the source code of this package: @@ -21297,7 +21322,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustc-demangle:0.1.23 +Package: rustc-demangle:0.1.24 The following copyrights and licenses were found in the source code of this package: @@ -21526,7 +21551,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls:0.22.3 +Package: rustls:0.22.4 The following copyrights and licenses were found in the source code of this package: @@ -21769,7 +21794,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-native-certs:0.7.0 +Package: rustls-native-certs:0.7.1 The following copyrights and licenses were found in the source code of this package: @@ -22255,7 +22280,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.4.1 +Package: rustls-pki-types:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -22484,7 +22509,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-webpki:0.102.2 +Package: rustls-webpki:0.102.5 The following copyrights and licenses were found in the source code of this package: @@ -22502,7 +22527,236 @@ THIS SOFTWARE. ---- -Package: ryu:1.0.17 +Package: rustversion:1.0.17 + +The following copyrights and licenses were found in the source code of this package: + + 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 [yyyy] [name of copyright owner] + + 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. + + -- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: ryu:1.0.18 The following copyrights and licenses were found in the source code of this package: @@ -22990,7 +23244,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: security-framework:2.10.0 +Package: security-framework:2.11.1 The following copyrights and licenses were found in the source code of this package: @@ -23219,7 +23473,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: security-framework-sys:2.10.0 +Package: security-framework-sys:2.11.1 The following copyrights and licenses were found in the source code of this package: @@ -23448,7 +23702,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde:1.0.198 +Package: serde:1.0.204 The following copyrights and licenses were found in the source code of this package: @@ -23677,7 +23931,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: serde_derive:1.0.198 +Package: serde_derive:1.0.204 The following copyrights and licenses were found in the source code of this package: @@ -24216,7 +24470,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: socket2:0.5.6 +Package: socket2:0.5.7 The following copyrights and licenses were found in the source code of this package: @@ -24470,7 +24724,57 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: subtle:2.5.0 +Package: strum:0.26.3 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: strum_macros:0.26.4 + +The following copyrights and licenses were found in the source code of this package: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---- + +Package: subtle:2.6.1 The following copyrights and licenses were found in the source code of this package: @@ -24730,7 +25034,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.60 +Package: syn:2.0.71 The following copyrights and licenses were found in the source code of this package: @@ -24959,7 +25263,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: target-lexicon:0.12.14 +Package: target-lexicon:0.12.15 The following copyrights and licenses were found in the source code of this package: @@ -25183,7 +25487,7 @@ Software. ---- -Package: thiserror:1.0.58 +Package: thiserror:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -25412,7 +25716,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: thiserror-impl:1.0.58 +Package: thiserror-impl:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -26557,7 +26861,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tinyvec:1.6.0 +Package: tinyvec:1.8.0 The following copyrights and licenses were found in the source code of this package: @@ -27055,7 +27359,7 @@ the following restrictions: ---- -Package: tokio:1.37.0 +Package: tokio:1.38.1 The following copyrights and licenses were found in the source code of this package: @@ -27080,7 +27384,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-macros:2.2.0 +Package: tokio-macros:2.3.0 The following copyrights and licenses were found in the source code of this package: @@ -27359,7 +27663,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: tokio-util:0.7.10 +Package: tokio-util:0.7.11 The following copyrights and licenses were found in the source code of this package: @@ -31980,7 +32284,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-targets:0.52.5 +Package: windows-targets:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32438,7 +32742,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_gnullvm:0.52.5 +Package: windows_aarch64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -32896,7 +33200,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.5 +Package: windows_aarch64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33354,7 +33658,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.5 +Package: windows_i686_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -33583,7 +33887,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnullvm:0.52.5 +Package: windows_i686_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34041,7 +34345,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.5 +Package: windows_i686_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34499,7 +34803,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.5 +Package: windows_x86_64_gnu:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -34957,7 +35261,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.52.5 +Package: windows_x86_64_gnullvm:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35415,7 +35719,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.52.5 +Package: windows_x86_64_msvc:0.52.6 The following copyrights and licenses were found in the source code of this package: @@ -35644,7 +35948,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy:0.7.32 +Package: zerocopy:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -35896,7 +36200,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zerocopy-derive:0.7.32 +Package: zerocopy-derive:0.7.35 The following copyrights and licenses were found in the source code of this package: @@ -36148,7 +36452,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: zeroize:1.7.0 +Package: zeroize:1.8.1 The following copyrights and licenses were found in the source code of this package: @@ -36610,7 +36914,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: black:22.10.0 +Package: black:24.4.2 The following copyrights and licenses were found in the source code of this package: @@ -36635,7 +36939,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: cachetools:5.3.3 +Package: cachetools:5.4.0 The following copyrights and licenses were found in the source code of this package: @@ -36660,7 +36964,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: certifi:2024.2.2 +Package: certifi:2024.7.4 The following copyrights and licenses were found in the source code of this package: @@ -37121,7 +37425,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: google-api-core:2.18.0 +Package: google-api-core:2.19.1 The following copyrights and licenses were found in the source code of this package: @@ -37537,7 +37841,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: google-auth:2.29.0 +Package: google-auth:2.32.0 The following copyrights and licenses were found in the source code of this package: @@ -37953,7 +38257,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: googleapis-common-protos:1.63.0 +Package: googleapis-common-protos:1.63.2 The following copyrights and licenses were found in the source code of this package: @@ -38748,238 +39052,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: packaging:21.3 - -The following copyrights and licenses were found in the source code of this package: - - 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 [yyyy] [name of copyright owner] - - 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. - - -- - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - -Package: packaging:24.0 +Package: packaging:24.1 The following copyrights and licenses were found in the source code of this package: @@ -39567,7 +39640,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---- -Package: platformdirs:4.2.0 +Package: platformdirs:4.2.2 The following copyrights and licenses were found in the source code of this package: @@ -39592,7 +39665,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pluggy:1.4.0 +Package: pluggy:1.5.0 The following copyrights and licenses were found in the source code of this package: @@ -39617,7 +39690,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: proto-plus:1.23.0 +Package: proto-plus:1.24.0 The following copyrights and licenses were found in the source code of this package: @@ -39856,7 +39929,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: protobuf:5.26.1 +Package: protobuf:5.27.2 The following copyrights and licenses were found in the source code of this package: @@ -40278,7 +40351,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: requests:2.31.0 +Package: requests:2.32.3 The following copyrights and licenses were found in the source code of this package: @@ -40719,7 +40792,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: types-protobuf:4.25.0.20240417 +Package: types-protobuf:5.27.0.20240626 The following copyrights and licenses were found in the source code of this package: @@ -40944,7 +41017,7 @@ PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 ---- -Package: typing-extensions:4.11.0 +Package: typing-extensions:4.12.2 The following copyrights and licenses were found in the source code of this package: @@ -41196,7 +41269,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: urllib3:2.2.1 +Package: urllib3:2.2.2 The following copyrights and licenses were found in the source code of this package: diff --git a/python/dev_requirements.txt b/python/dev_requirements.txt index 07aab8ee6e..36f3438740 100644 --- a/python/dev_requirements.txt +++ b/python/dev_requirements.txt @@ -1,6 +1,6 @@ -black == 22.10 +black >= 24.3.0 flake8 == 5.0 isort == 5.10 mypy == 1.2 mypy-protobuf == 3.5 -packaging==21.3 +packaging >= 22.0 diff --git a/python/pyproject.toml b/python/pyproject.toml index d0cf04d817..c0399197b8 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=0.13,<0.14"] build-backend = "maturin" [project] -name = "glide-for-redis" +name = "valkey-glide" requires-python = ">=3.8" dependencies = [ "async-timeout>=4.0.2", @@ -11,7 +11,6 @@ dependencies = [ "google-api-python-client==2.85.0" ] classifiers = [ - "Development Status :: 4 - Beta", "Topic :: Database", "Topic :: Utilities", "License :: OSI Approved :: Apache Software License", @@ -32,3 +31,6 @@ extend-ignore = ['E203'] [tool.black] target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] + +[tool.mypy] +exclude = [ 'submodules' ] diff --git a/python/pytest.ini b/python/pytest.ini index bf42185756..0624078cea 100644 --- a/python/pytest.ini +++ b/python/pytest.ini @@ -1,4 +1,4 @@ [pytest] markers = smoke_test: mark a test as a build verification testing. -addopts = -k "not redis_modules" +addopts = -k "not server_modules and not pubsub" diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index 83828fea88..f1f758aeed 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -1,44 +1,94 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +from glide.async_commands.bitmap import ( + BitEncoding, + BitFieldGet, + BitFieldIncrBy, + BitFieldOffset, + BitFieldOverflow, + BitFieldSet, + BitFieldSubCommands, + BitmapIndexType, + BitOffset, + BitOffsetMultiplier, + BitOverflowControl, + BitwiseOperation, + OffsetOptions, + SignedEncoding, + UnsignedEncoding, +) +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, + ExpiryGetEx, ExpirySet, ExpiryType, - GeospatialData, + ExpiryTypeGetEx, + FlushMode, + FunctionRestorePolicy, InfoSection, + InsertPosition, UpdateOptions, ) -from glide.async_commands.redis_modules import json +from glide.async_commands.server_modules import json from glide.async_commands.sorted_set import ( + AggregationType, + GeoSearchByBox, + GeoSearchByRadius, + GeoSearchCount, + GeospatialData, + GeoUnit, InfBound, LexBoundary, - Limit, RangeByIndex, RangeByLex, RangeByScore, ScoreBoundary, + ScoreFilter, +) +from glide.async_commands.stream import ( + ExclusiveIdBound, + IdBound, + MaxId, + MinId, + StreamAddOptions, + StreamClaimOptions, + StreamGroupOptions, + StreamPendingOptions, + StreamRangeBound, + StreamReadGroupOptions, + StreamReadOptions, + StreamTrimOptions, + TrimByMaxLen, + TrimByMinId, ) from glide.async_commands.transaction import ClusterTransaction, Transaction from glide.config import ( + BackoffStrategy, BaseClientConfiguration, - ClusterClientConfiguration, + GlideClientConfiguration, + GlideClusterClientConfiguration, NodeAddress, + PeriodicChecksManualInterval, + PeriodicChecksStatus, + ProtocolVersion, ReadFrom, - RedisClientConfiguration, - RedisCredentials, + ServerCredentials, ) from glide.constants import OK from glide.exceptions import ( ClosingError, + ConfigurationError, + ConnectionError, ExecAbortError, - RedisError, + GlideError, RequestError, TimeoutError, ) +from glide.glide_client import GlideClient, GlideClusterClient from glide.logger import Level as LogLevel from glide.logger import Logger -from glide.redis_client import RedisClient, RedisClusterClient from glide.routes import ( AllNodes, AllPrimaries, @@ -49,38 +99,90 @@ SlotType, ) -from .glide import Script +from .glide import ClusterScanCursor, Script __all__ = [ + # Client + "GlideClient", + "GlideClusterClient", + "Transaction", + "ClusterTransaction", + # Config "BaseClientConfiguration", - "ClusterClientConfiguration", - "RedisClientConfiguration", + "GlideClientConfiguration", + "GlideClusterClientConfiguration", + "BackoffStrategy", + "ReadFrom", + "ServerCredentials", + "NodeAddress", + "ProtocolVersion", + "PeriodicChecksManualInterval", + "PeriodicChecksStatus", + # Response + "OK", + # Commands + "BitEncoding", + "BitFieldGet", + "BitFieldIncrBy", + "BitFieldOffset", + "BitFieldOverflow", + "BitFieldSet", + "BitFieldSubCommands", + "BitmapIndexType", + "BitOffset", + "BitOffsetMultiplier", + "BitOverflowControl", + "BitwiseOperation", + "OffsetOptions", + "SignedEncoding", + "UnsignedEncoding", + "Script", "ScoreBoundary", "ConditionalChange", - "GeospatialData", "ExpireOptions", + "ExpiryGetEx", "ExpirySet", "ExpiryType", + "ExpiryTypeGetEx", + "FlushMode", + "FunctionRestorePolicy", + "GeoSearchByBox", + "GeoSearchByRadius", + "GeoSearchCount", + "GeoUnit", + "GeospatialData", + "AggregationType", "InfBound", "InfoSection", + "InsertPosition", "json", "LexBoundary", "Limit", + "ListDirection", "RangeByIndex", "RangeByLex", "RangeByScore", + "ScoreFilter", + "OrderBy", + "ExclusiveIdBound", + "IdBound", + "MaxId", + "MinId", + "StreamAddOptions", + "StreamClaimOptions", + "StreamGroupOptions", + "StreamPendingOptions", + "StreamReadGroupOptions", + "StreamRangeBound", + "StreamReadOptions", + "StreamTrimOptions", + "TrimByMaxLen", + "TrimByMinId", "UpdateOptions", + "ClusterScanCursor" + # Logger "Logger", "LogLevel", - "OK", - "ReadFrom", - "RedisClient", - "RedisClusterClient", - "RedisCredentials", - "Script", - "NodeAddress", - "Transaction", - "ClusterTransaction", # Routes "SlotType", "AllNodes", @@ -91,8 +193,10 @@ "SlotIdRoute", # Exceptions "ClosingError", + "ConfigurationError", + "ConnectionError", "ExecAbortError", - "RedisError", + "GlideError", "RequestError", "TimeoutError", ] diff --git a/python/python/glide/async_commands/__init__.py b/python/python/glide/async_commands/__init__.py index 3779cd3fa5..8aaf21baff 100644 --- a/python/python/glide/async_commands/__init__.py +++ b/python/python/glide/async_commands/__init__.py @@ -1,4 +1,4 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from .core import CoreCommands diff --git a/python/python/glide/async_commands/bitmap.py b/python/python/glide/async_commands/bitmap.py new file mode 100644 index 0000000000..46fdf61c70 --- /dev/null +++ b/python/python/glide/async_commands/bitmap.py @@ -0,0 +1,305 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +from abc import ABC, abstractmethod +from enum import Enum +from typing import List, Optional + + +class BitmapIndexType(Enum): + """ + Enumeration specifying if index arguments are BYTE indexes or BIT indexes. Can be specified in `OffsetOptions`, + which is an optional argument to the `BITCOUNT` command. + + Since: Valkey version 7.0.0. + """ + + BYTE = "BYTE" + """ + Specifies that indexes provided to `OffsetOptions` are byte indexes. + """ + BIT = "BIT" + """ + Specifies that indexes provided to `OffsetOptions` are bit indexes. + """ + + +class OffsetOptions: + def __init__( + self, start: int, end: int, index_type: Optional[BitmapIndexType] = None + ): + """ + Represents offsets specifying a string interval to analyze in the `BITCOUNT` command. The offsets are + zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. + The offsets can also be negative numbers indicating offsets starting at the end of the string, with `-1` being + the last index of the string, `-2` being the penultimate, and so on. + + Args: + start (int): The starting offset index. + end (int): The ending offset index. + index_type (Optional[BitmapIndexType]): The index offset type. This option can only be specified if you are + using Valkey version 7.0.0 or above. Could be either `BitmapIndexType.BYTE` or `BitmapIndexType.BIT`. + If no index type is provided, the indexes will be assumed to be byte indexes. + """ + self.start = start + self.end = end + self.index_type = index_type + + def to_args(self) -> List[str]: + args = [str(self.start), str(self.end)] + if self.index_type is not None: + args.append(self.index_type.value) + + return args + + +class BitwiseOperation(Enum): + """ + Enumeration defining the bitwise operation to use in the `BITOP` command. Specifies the bitwise operation to + perform between the passed in keys. + """ + + AND = "AND" + OR = "OR" + XOR = "XOR" + NOT = "NOT" + + +class BitEncoding(ABC): + """ + Abstract Base Class used to specify a signed or unsigned argument encoding for the `BITFIELD` or `BITFIELD_RO` + commands. + """ + + @abstractmethod + def to_arg(self) -> str: + """ + Returns the encoding as a string argument to be used in the `BITFIELD` or `BITFIELD_RO` + commands. + """ + pass + + +class SignedEncoding(BitEncoding): + # Prefix specifying that the encoding is signed. + SIGNED_ENCODING_PREFIX = "i" + + def __init__(self, encoding_length: int): + """ + Represents a signed argument encoding. Must be less than 65 bits long. + + Args: + encoding_length (int): The bit size of the encoding. + """ + self._encoding = f"{self.SIGNED_ENCODING_PREFIX}{str(encoding_length)}" + + def to_arg(self) -> str: + return self._encoding + + +class UnsignedEncoding(BitEncoding): + # Prefix specifying that the encoding is unsigned. + UNSIGNED_ENCODING_PREFIX = "u" + + def __init__(self, encoding_length: int): + """ + Represents an unsigned argument encoding. Must be less than 64 bits long. + + Args: + encoding_length (int): The bit size of the encoding. + """ + self._encoding = f"{self.UNSIGNED_ENCODING_PREFIX}{str(encoding_length)}" + + def to_arg(self) -> str: + return self._encoding + + +class BitFieldOffset(ABC): + """Abstract Base Class representing an offset for an array of bits for the `BITFIELD` or `BITFIELD_RO` commands.""" + + @abstractmethod + def to_arg(self) -> str: + """ + Returns the offset as a string argument to be used in the `BITFIELD` or `BITFIELD_RO` + commands. + """ + pass + + +class BitOffset(BitFieldOffset): + def __init__(self, offset: int): + """ + Represents an offset in an array of bits for the `BITFIELD` or `BITFIELD_RO` commands. Must be greater than or + equal to 0. + + For example, if we have the binary `01101001` with offset of 1 for an unsigned encoding of size 4, then the value + is 13 from `0(1101)001`. + + Args: + offset (int): The bit index offset in the array of bits. + """ + self._offset = str(offset) + + def to_arg(self) -> str: + return self._offset + + +class BitOffsetMultiplier(BitFieldOffset): + # Prefix specifying that the offset uses an encoding multiplier. + OFFSET_MULTIPLIER_PREFIX = "#" + + def __init__(self, offset: int): + """ + Represents an offset in an array of bits for the `BITFIELD` or `BITFIELD_RO` commands. The bit offset index is + calculated as the numerical value of the offset multiplied by the encoding value. Must be greater than or equal + to 0. + + For example, if we have the binary 01101001 with offset multiplier of 1 for an unsigned encoding of size 4, then + the value is 9 from `0110(1001)`. + + Args: + offset (int): The offset in the array of bits, which will be multiplied by the encoding value to get the + final bit index offset. + """ + self._offset = f"{self.OFFSET_MULTIPLIER_PREFIX}{str(offset)}" + + def to_arg(self) -> str: + return self._offset + + +class BitFieldSubCommands(ABC): + """Abstract Base Class representing subcommands for the `BITFIELD` or `BITFIELD_RO` commands.""" + + @abstractmethod + def to_args(self) -> List[str]: + """ + Returns the subcommand as a list of string arguments to be used in the `BITFIELD` or `BITFIELD_RO` commands. + """ + pass + + +class BitFieldGet(BitFieldSubCommands): + # "GET" subcommand string for use in the `BITFIELD` or `BITFIELD_RO` commands. + GET_COMMAND_STRING = "GET" + + def __init__(self, encoding: BitEncoding, offset: BitFieldOffset): + """ + Represents the "GET" subcommand for getting a value in the binary representation of the string stored in `key`. + + Args: + encoding (BitEncoding): The bit encoding for the subcommand. + offset (BitFieldOffset): The offset in the array of bits from which to get the value. + """ + self._encoding = encoding + self._offset = offset + + def to_args(self) -> List[str]: + return [self.GET_COMMAND_STRING, self._encoding.to_arg(), self._offset.to_arg()] + + +class BitFieldSet(BitFieldSubCommands): + # "SET" subcommand string for use in the `BITFIELD` command. + SET_COMMAND_STRING = "SET" + + def __init__(self, encoding: BitEncoding, offset: BitFieldOffset, value: int): + """ + Represents the "SET" subcommand for setting bits in the binary representation of the string stored in `key`. + + Args: + encoding (BitEncoding): The bit encoding for the subcommand. + offset (BitOffset): The offset in the array of bits where the value will be set. + value (int): The value to set the bits in the binary value to. + """ + self._encoding = encoding + self._offset = offset + self._value = value + + def to_args(self) -> List[str]: + return [ + self.SET_COMMAND_STRING, + self._encoding.to_arg(), + self._offset.to_arg(), + str(self._value), + ] + + +class BitFieldIncrBy(BitFieldSubCommands): + # "INCRBY" subcommand string for use in the `BITFIELD` command. + INCRBY_COMMAND_STRING = "INCRBY" + + def __init__(self, encoding: BitEncoding, offset: BitFieldOffset, increment: int): + """ + Represents the "INCRBY" subcommand for increasing or decreasing bits in the binary representation of the + string stored in `key`. + + Args: + encoding (BitEncoding): The bit encoding for the subcommand. + offset (BitOffset): The offset in the array of bits where the value will be incremented. + increment (int): The value to increment the bits in the binary value by. + """ + self._encoding = encoding + self._offset = offset + self._increment = increment + + def to_args(self) -> List[str]: + return [ + self.INCRBY_COMMAND_STRING, + self._encoding.to_arg(), + self._offset.to_arg(), + str(self._increment), + ] + + +class BitOverflowControl(Enum): + """ + Enumeration specifying bit overflow controls for the `BITFIELD` command. + """ + + WRAP = "WRAP" + """ + Performs modulo when overflows occur with unsigned encoding. When overflows occur with signed encoding, the value + restarts at the most negative value. When underflows occur with signed encoding, the value restarts at the most + positive value. + """ + SAT = "SAT" + """ + Underflows remain set to the minimum value, and overflows remain set to the maximum value. + """ + FAIL = "FAIL" + """ + Returns `None` when overflows occur. + """ + + +class BitFieldOverflow(BitFieldSubCommands): + # "OVERFLOW" subcommand string for use in the `BITFIELD` command. + OVERFLOW_COMMAND_STRING = "OVERFLOW" + + def __init__(self, overflow_control: BitOverflowControl): + """ + Represents the "OVERFLOW" subcommand that determines the result of the "SET" or "INCRBY" `BITFIELD` subcommands + when an underflow or overflow occurs. + + Args: + overflow_control (BitOverflowControl): The desired overflow behavior. + """ + self._overflow_control = overflow_control + + def to_args(self) -> List[str]: + return [self.OVERFLOW_COMMAND_STRING, self._overflow_control.value] + + +def _create_bitfield_args(subcommands: List[BitFieldSubCommands]) -> List[str]: + args = [] + for subcommand in subcommands: + args.extend(subcommand.to_args()) + + return args + + +def _create_bitfield_read_only_args( + subcommands: List[BitFieldGet], +) -> List[str]: + args = [] + for subcommand in subcommands: + args.extend(subcommand.to_args()) + + return args diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index e3727adadb..a0044c3e92 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -1,34 +1,53 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from __future__ import annotations -from typing import Dict, List, Mapping, Optional, cast +from typing import Any, Dict, List, Mapping, Optional, Set, Union, cast -from glide.async_commands.core import CoreCommands, InfoSection -from glide.async_commands.transaction import BaseTransaction, ClusterTransaction -from glide.constants import TOK, TClusterResponse, TResult, TSingleNodeRoute -from glide.protobuf.redis_request_pb2 import RequestType +from glide.async_commands.command_args import Limit, ObjectType, OrderBy +from glide.async_commands.core import ( + CoreCommands, + FlushMode, + FunctionRestorePolicy, + InfoSection, + _build_sort_args, +) +from glide.async_commands.transaction import ClusterTransaction +from glide.constants import ( + TOK, + TClusterResponse, + TEncodable, + TFunctionListResponse, + TFunctionStatsResponse, + TResult, + TSingleNodeRoute, +) +from glide.protobuf.command_request_pb2 import RequestType from glide.routes import Route +from ..glide import ClusterScanCursor + class ClusterCommands(CoreCommands): async def custom_command( - self, command_args: List[str], route: Optional[Route] = None + self, command_args: List[TEncodable], route: Optional[Route] = None ) -> TResult: """ Executes a single command, without checking inputs. - @remarks - This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + See the [Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + for details on the restrictions and limitations of the custom command API. + @example - Return a list of all pub/sub clients from all nodes: connection.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"], AllNodes()) Args: - command_args (List[str]): List of strings of the command's arguments. + command_args (List[TEncodable]): List of the command's arguments, where each argument is either a string or bytes. Every part of the command, including the command name and subcommands, should be added as a separate value in args. route (Optional[Route]): The command will be routed automatically based on the passed command's default request policy, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Defaults to None. Returns: - TResult: The returning value depends on the executed command and the route + TResult: The returning value depends on the executed command and the route. """ return await self._execute_command( RequestType.CustomCommand, command_args, route @@ -38,10 +57,10 @@ async def info( self, sections: Optional[List[InfoSection]] = None, route: Optional[Route] = None, - ) -> TClusterResponse[str]: + ) -> TClusterResponse[bytes]: """ - Get information and statistics about the Redis server. - See https://redis.io/commands/info/ for details. + Get information and statistics about the server. + See https://valkey.io/commands/info/ for details. Args: sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of @@ -50,37 +69,38 @@ async def info( case the client will route the command to the nodes defined by `route`. Defaults to None. Returns: - TClusterResponse[str]: If a single node route is requested, returns a string containing the information for - the required sections. Otherwise, returns a dict of strings, with each key containing the address of + TClusterResponse[bytes]: If a single node route is requested, returns a bytes string containing the information for + the required sections. Otherwise, returns a dict of bytes strings, with each key containing the address of the queried node and value containing the information regarding the requested sections. """ - args = [section.value for section in sections] if sections else [] - + args: List[TEncodable] = ( + [section.value for section in sections] if sections else [] + ) return cast( - TClusterResponse[str], + TClusterResponse[bytes], await self._execute_command(RequestType.Info, args, route), ) async def exec( self, - transaction: BaseTransaction | ClusterTransaction, + transaction: ClusterTransaction, route: Optional[TSingleNodeRoute] = None, ) -> Optional[List[TResult]]: """ Execute a transaction by processing the queued commands. - See https://redis.io/topics/Transactions/ for details on Redis Transactions. + See https://valkey.io/docs/topics/transactions/ for details on Transactions. Args: - transaction (ClusterTransaction): A ClusterTransaction object containing a list of commands to be executed. + transaction (ClusterTransaction): A `ClusterTransaction` object containing a list of commands to be executed. route (Optional[TSingleNodeRoute]): If `route` is not provided, the transaction will be routed to the slot owner of the - first key found in the transaction. If no key is found, the command will be sent to a random node. - If `route` is provided, the client will route the command to the nodes defined by `route`. + first key found in the transaction. If no key is found, the command will be sent to a random node. + If `route` is provided, the client will route the command to the nodes defined by `route`. Returns: Optional[List[TResult]]: A list of results corresponding to the execution of each command - in the transaction. If a command returns a value, it will be included in the list. If a command - doesn't return a value, the list entry will be None. - If the transaction failed due to a WATCH command, `exec` will return `None`. + in the transaction. If a command returns a value, it will be included in the list. If a command + doesn't return a value, the list entry will be `None`. + If the transaction failed due to a WATCH command, `exec` will return `None`. """ commands = transaction.commands[:] return await self._execute_transaction(commands, route) @@ -90,8 +110,8 @@ async def config_resetstat( route: Optional[Route] = None, ) -> TOK: """ - Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - See https://redis.io/commands/config-resetstat/ for details. + Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands. + See https://valkey.io/commands/config-resetstat/ for details. Args: route (Optional[Route]): The command will be routed automatically to all nodes, unless `route` is provided, in which @@ -110,7 +130,7 @@ async def config_rewrite( ) -> TOK: """ Rewrite the configuration file with the current configuration. - See https://redis.io/commands/config-rewrite/ for details. + See https://valkey.io/commands/config-rewrite/ for details. Args: route (Optional[TRoute]): The command will be routed automatically to all nodes, unless `route` is provided, in which @@ -133,7 +153,7 @@ async def client_id( ) -> TClusterResponse[int]: """ Returns the current connection id. - See https://redis.io/commands/client-id/ for more information. + See https://valkey.io/commands/client-id/ for more information. Args: route (Optional[Route]): The command will be sent to a random node, unless `route` is provided, in which @@ -142,7 +162,7 @@ async def client_id( Returns: TClusterResponse[int]: The id of the client. If a single node route is requested, returns a int representing the client's id. - Otherwise, returns a dict of [str , int] where each key contains the address of + Otherwise, returns a dict of [byte , int] where each key contains the address of the queried node and the value contains the client's id. """ return cast( @@ -151,70 +171,74 @@ async def client_id( ) async def ping( - self, message: Optional[str] = None, route: Optional[Route] = None - ) -> str: + self, message: Optional[TEncodable] = None, route: Optional[Route] = None + ) -> bytes: """ - Ping the Redis server. - See https://redis.io/commands/ping/ for more details. + Ping the server. + See https://valkey.io/commands/ping/ for more details. Args: - message (Optional[str]): An optional message to include in the PING command. If not provided, - the server will respond with "PONG". If provided, the server will respond with a copy of the message + message (Optional[TEncodable]): An optional message to include in the PING command. If not provided, + the server will respond with b"PONG". If provided, the server will respond with a copy of the message. route (Optional[Route]): The command will be sent to all primaries, unless `route` is provided, in which case the client will route the command to the nodes defined by `route` Returns: - str: "PONG" if `message` is not provided, otherwise return a copy of `message`. + bytes: b'PONG' if `message` is not provided, otherwise return a copy of `message`. Examples: >>> await client.ping() - "PONG" + b"PONG" >>> await client.ping("Hello") - "Hello" + b"Hello" """ argument = [] if message is None else [message] - return cast(str, await self._execute_command(RequestType.Ping, argument, route)) + return cast( + bytes, await self._execute_command(RequestType.Ping, argument, route) + ) async def config_get( - self, parameters: List[str], route: Optional[Route] = None - ) -> TClusterResponse[Dict[str, str]]: + self, parameters: List[TEncodable], route: Optional[Route] = None + ) -> TClusterResponse[Dict[bytes, bytes]]: """ Get the values of configuration parameters. - See https://redis.io/commands/config-get/ for details. + See https://valkey.io/commands/config-get/ for details. Args: - parameters (List[str]): A list of configuration parameter names to retrieve values for. + parameters (List[TEncodable]): A list of configuration parameter names to retrieve values for. route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Returns: - TClusterResponse[Dict[str, str]]: A dictionary of values corresponding to the + TClusterResponse[Dict[bytes, bytes]]: A dictionary of values corresponding to the configuration parameters. - When specifying a route other than a single node, response will be : {Address (str) : response (Dict[str, str]) , ... } - with type of Dict[str, Dict[str, str]]. + When specifying a route other than a single node, response will be : {Address (bytes) : response (Dict[bytes, bytes]) , ... } + with type of Dict[bytes, Dict[bytes, bytes]]. Examples: >>> await client.config_get(["timeout"] , RandomNode()) - {'timeout': '1000'} - >>> await client.config_get(["timeout" , "maxmemory"]) - {'timeout': '1000', "maxmemory": "1GB"} + {b'timeout': b'1000'} + >>> await client.config_get(["timeout" , b"maxmemory"]) + {b'timeout': b'1000', b"maxmemory": b"1GB"} """ return cast( - TClusterResponse[Dict[str, str]], + TClusterResponse[Dict[bytes, bytes]], await self._execute_command(RequestType.ConfigGet, parameters, route), ) async def config_set( - self, parameters_map: Mapping[str, str], route: Optional[Route] = None + self, + parameters_map: Mapping[TEncodable, TEncodable], + route: Optional[Route] = None, ) -> TOK: """ Set configuration parameters to the specified values. - See https://redis.io/commands/config-set/ for details. + See https://valkey.io/commands/config-set/ for details. Args: - parameters_map (Mapping[str, str]): A map consisting of configuration + parameters_map (Mapping[TEncodable, TEncodable]): A map consisting of configuration parameters and their respective values to set. route (Optional[Route]): The command will be routed to all nodes, unless `route` is provided, @@ -224,10 +248,10 @@ async def config_set( OK: Returns OK if all configurations have been successfully set. Otherwise, raises an error. Examples: - >>> await client.config_set([("timeout", "1000")], [("maxmemory", "1GB")]) - OK + >>> await client.config_set({"timeout": "1000", b"maxmemory": b"1GB"}) + OK """ - parameters: List[str] = [] + parameters: List[TEncodable] = [] for pair in parameters_map.items(): parameters.extend(pair) return cast( @@ -237,36 +261,36 @@ async def config_set( async def client_getname( self, route: Optional[Route] = None - ) -> TClusterResponse[Optional[str]]: + ) -> TClusterResponse[Optional[bytes]]: """ Get the name of the connection to which the request is routed. - See https://redis.io/commands/client-getname/ for more details. + See https://valkey.io/commands/client-getname/ for more details. Args: route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Returns: - TClusterResponse[Optional[str]]: The name of the client connection as a string if a name is set, + TClusterResponse[Optional[bytes]]: The name of the client connection as a bytes string if a name is set, or None if no name is assigned. When specifying a route other than a single node, response will be: - {Address (str) : response (Optional[str]) , ... } with type of Dict[str, Optional[str]]. + {Address (bytes) : response (Optional[bytes]) , ... } with type of Dict[str, Optional[str]]. Examples: >>> await client.client_getname() - 'Connection Name' + b'Connection Name' >>> await client.client_getname(AllNodes()) - {'addr': 'Connection Name', 'addr2': 'Connection Name', 'addr3': 'Connection Name'} + {b'addr': b'Connection Name', b'addr2': b'Connection Name', b'addr3': b'Connection Name'} """ return cast( - TClusterResponse[Optional[str]], + TClusterResponse[Optional[bytes]], await self._execute_command(RequestType.ClientGetName, [], route), ) async def dbsize(self, route: Optional[Route] = None) -> int: """ Returns the number of keys in the database. - See https://redis.io/commands/dbsize for more details. + See https://valkey.io/commands/dbsize for more details. Args: route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, @@ -283,58 +307,905 @@ async def dbsize(self, route: Optional[Route] = None) -> int: return cast(int, await self._execute_command(RequestType.DBSize, [], route)) async def echo( - self, message: str, route: Optional[Route] = None - ) -> TClusterResponse[str]: + self, message: TEncodable, route: Optional[Route] = None + ) -> TClusterResponse[bytes]: """ Echoes the provided `message` back. - See https://redis.io/commands/echo for more details. + See https://valkey.io/commands/echo for more details. Args: - message (str): The message to be echoed back. + message (TEncodable): The message to be echoed back. route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Returns: - TClusterResponse[str]: The provided `message`. + TClusterResponse[bytes]: The provided `message`. When specifying a route other than a single node, response will be: - {Address (str) : response (str) , ... } with type of Dict[str, str]. + {Address (bytes) : response (bytes) , ... } with type of Dict[bytes, bytes]. Examples: - >>> await client.echo("Glide-for-Redis") - 'Glide-for-Redis' - >>> await client.echo("Glide-for-Redis", AllNodes()) - {'addr': 'Glide-for-Redis', 'addr2': 'Glide-for-Redis', 'addr3': 'Glide-for-Redis'} + >>> await client.echo(b"Valkey GLIDE") + b'Valkey GLIDE' + >>> await client.echo("Valkey GLIDE", AllNodes()) + {b'addr': b'Valkey GLIDE', b'addr2': b'Valkey GLIDE', b'addr3': b'Valkey GLIDE'} """ return cast( - TClusterResponse[str], + TClusterResponse[bytes], await self._execute_command(RequestType.Echo, [message], route), ) - async def time(self, route: Optional[Route] = None) -> TClusterResponse[List[str]]: + async def function_load( + self, + library_code: TEncodable, + replace: bool = False, + route: Optional[Route] = None, + ) -> bytes: + """ + Loads a library to Valkey. + + See https://valkey.io/commands/function-load/ for more details. + + Args: + library_code (TEncodable): The source code that implements the library. + replace (bool): Whether the given library should overwrite a library with the same name if + it already exists. + route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + bytes: The library name that was loaded. + + Examples: + >>> code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + >>> await client.function_load(code, True, RandomNode()) + b"mylib" + + Since: Valkey 7.0.0. + """ + return cast( + bytes, + await self._execute_command( + RequestType.FunctionLoad, + ["REPLACE", library_code] if replace else [library_code], + route, + ), + ) + + async def function_list( + self, + library_name_pattern: Optional[TEncodable] = None, + with_code: bool = False, + route: Optional[Route] = None, + ) -> TClusterResponse[TFunctionListResponse]: + """ + Returns information about the functions and libraries. + + See https://valkey.io/commands/function-list/ for more details. + + Args: + library_name_pattern (Optional[TEncodable]): A wildcard pattern for matching library names. + with_code (bool): Specifies whether to request the library code from the server or not. + route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TClusterResponse[TFunctionListResponse]: Info + about all or selected libraries and their functions. + + Examples: + >>> response = await client.function_list("myLib?_backup", True) + [{ + b"library_name": b"myLib5_backup", + b"engine": b"LUA", + b"functions": [{ + b"name": b"myfunc", + b"description": None, + b"flags": {b"no-writes"}, + }], + b"library_code": b"#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + }] + + Since: Valkey 7.0.0. + """ + args = [] + if library_name_pattern is not None: + args.extend(["LIBRARYNAME", library_name_pattern]) + if with_code: + args.append("WITHCODE") + return cast( + TClusterResponse[TFunctionListResponse], + await self._execute_command( + RequestType.FunctionList, + args, + route, + ), + ) + + async def function_flush( + self, mode: Optional[FlushMode] = None, route: Optional[Route] = None + ) -> TOK: + """ + Deletes all function libraries. + + See https://valkey.io/commands/function-flush/ for more details. + + Args: + mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TOK: A simple `OK`. + + Examples: + >>> await client.function_flush(FlushMode.SYNC) + "OK" + + Since: Valkey 7.0.0. + """ + return cast( + TOK, + await self._execute_command( + RequestType.FunctionFlush, + [mode.value] if mode else [], + route, + ), + ) + + async def function_delete( + self, library_name: TEncodable, route: Optional[Route] = None + ) -> TOK: + """ + Deletes a library and all its functions. + + See https://valkey.io/commands/function-delete/ for more details. + + Args: + library_code (TEncodable): The library name to delete + route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TOK: A simple `OK`. + + Examples: + >>> await client.function_delete("my_lib") + "OK" + + Since: Valkey 7.0.0. + """ + return cast( + TOK, + await self._execute_command( + RequestType.FunctionDelete, + [library_name], + route, + ), + ) + + async def function_kill(self, route: Optional[Route] = None) -> TOK: + """ + Kills a function that is currently executing. + This command only terminates read-only functions. + + See https://valkey.io/commands/function-kill/ for more details. + + Args: + route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TOK: A simple `OK`. + + Examples: + >>> await client.function_kill() + "OK" + + Since: Valkey 7.0.0. + """ + return cast( + TOK, + await self._execute_command( + RequestType.FunctionKill, + [], + route, + ), + ) + + async def fcall_route( + self, + function: TEncodable, + arguments: Optional[List[TEncodable]] = None, + route: Optional[Route] = None, + ) -> TClusterResponse[TResult]: + """ + Invokes a previously loaded function. + See https://valkey.io/commands/fcall/ for more details. + + Args: + function (TEncodable): The function name. + arguments (Optional[List[TEncodable]]): A list of `function` arguments. `Arguments` + should not represent names of keys. + route (Optional[Route]): The command will be routed to a random primary node, unless `route` is provided, in which + case the client will route the command to the nodes defined by `route`. Defaults to None. + + Returns: + TClusterResponse[TResult]: + If a single node route is requested, returns a Optional[TResult] representing the function's return value. + Otherwise, returns a dict of [bytes , Optional[TResult]] where each key contains the address of + the queried node and the value contains the function's return value. + + Example: + >>> await client.fcall("Deep_Thought", ["Answer", "to", "the", "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"], RandomNode()) + b'new_value' # Returns the function's return value. + + Since: Valkey version 7.0.0. + """ + args = [function, "0"] + if arguments is not None: + args.extend(arguments) + return cast( + TClusterResponse[TResult], + await self._execute_command(RequestType.FCall, args, route), + ) + + async def fcall_ro_route( + self, + function: TEncodable, + arguments: Optional[List[TEncodable]] = None, + route: Optional[Route] = None, + ) -> TClusterResponse[TResult]: + """ + Invokes a previously loaded read-only function. + + See https://valkey.io/commands/fcall_ro for more details. + + Args: + function (TEncodable): The function name. + arguments (List[TEncodable]): An `array` of `function` arguments. `arguments` should not + represent names of keys. + route (Optional[Route]): Specifies the routing configuration of the command. The client + will route the command to the nodes defined by `route`. + + Returns: + TClusterResponse[TResult]: The return value depends on the function that was executed. + + Examples: + >>> await client.fcall_ro_route("Deep_Thought", ALL_NODES) + 42 # The return value on the function that was executed + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [function, "0"] + if arguments is not None: + args.extend(arguments) + return cast( + TClusterResponse[TResult], + await self._execute_command(RequestType.FCallReadOnly, args, route), + ) + + async def function_stats( + self, route: Optional[Route] = None + ) -> TClusterResponse[TFunctionStatsResponse]: + """ + Returns information about the function that's currently running and information about the + available execution engines. + + See https://valkey.io/commands/function-stats/ for more details + + Args: + route (Optional[Route]): Specifies the routing configuration for the command. The client + will route the command to the nodes defined by `route`. + + Returns: + TClusterResponse[TFunctionStatsResponse]: A `Mapping` with two keys: + - `running_script` with information about the running script. + - `engines` with information about available engines and their stats. + See example for more details. + + Examples: + >>> await client.function_stats(RandomNode()) + { + 'running_script': { + 'name': 'foo', + 'command': ['FCALL', 'foo', '0', 'hello'], + 'duration_ms': 7758 + }, + 'engines': { + 'LUA': { + 'libraries_count': 1, + 'functions_count': 1, + } + } + } + + Since: Valkey version 7.0.0. + """ + return cast( + TClusterResponse[TFunctionStatsResponse], + await self._execute_command(RequestType.FunctionStats, [], route), + ) + + async def function_dump( + self, route: Optional[Route] = None + ) -> TClusterResponse[bytes]: + """ + Returns the serialized payload of all loaded libraries. + + See https://valkey.io/commands/function-dump/ for more details. + + Args: + route (Optional[Route]): The command will be routed to a random node, unless + `route` is provided, in which case the client will route the command to the + nodes defined by `route`. + + Returns: + TClusterResponse[bytes]: The serialized payload of all loaded libraries. + + Examples: + >>> payload = await client.function_dump() + # The serialized payload of all loaded libraries. This response can + # be used to restore loaded functions on any Valkey instance. + >>> await client.function_restore(payload) + "OK" # The serialized dump response was used to restore the libraries. + + Since: Valkey 7.0.0. + """ + return cast( + TClusterResponse[bytes], + await self._execute_command(RequestType.FunctionDump, [], route), + ) + + async def function_restore( + self, + payload: TEncodable, + policy: Optional[FunctionRestorePolicy] = None, + route: Optional[Route] = None, + ) -> TOK: + """ + Restores libraries from the serialized payload returned by the `function_dump` command. + + See https://valkey.io/commands/function-restore/ for more details. + + Args: + payload (bytes): The serialized data from the `function_dump` command. + policy (Optional[FunctionRestorePolicy]): A policy for handling existing libraries. + route (Optional[Route]): The command will be sent to all primaries, unless + `route` is provided, in which case the client will route the command to the + nodes defined by `route`. + + Returns: + TOK: OK. + + Examples: + >>> payload = await client.function_dump() + # The serialized payload of all loaded libraries. This response can + # be used to restore loaded functions on any Valkey instance. + >>> await client.function_restore(payload, AllPrimaries()) + "OK" # The serialized dump response was used to restore the libraries with the specified route. + >>> await client.function_restore(payload, FunctionRestorePolicy.FLUSH, AllPrimaries()) + "OK" # The serialized dump response was used to restore the libraries with the specified route and policy. + + Since: Valkey 7.0.0. + """ + args: List[TEncodable] = [payload] + if policy is not None: + args.append(policy.value) + + return cast( + TOK, await self._execute_command(RequestType.FunctionRestore, args, route) + ) + + async def time( + self, route: Optional[Route] = None + ) -> TClusterResponse[List[bytes]]: """ Returns the server time. - See https://redis.io/commands/time/ for more details. + See https://valkey.io/commands/time/ for more details. Args: route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, in which case the client will route the command to the nodes defined by `route`. Returns: - TClusterResponse[Optional[str]]: The current server time as a two items `array`: + TClusterResponse[Optional[bytes]]: The current server time as a two items `array`: A Unix timestamp and the amount of microseconds already elapsed in the current second. The returned `array` is in a [Unix timestamp, Microseconds already elapsed] format. When specifying a route other than a single node, response will be: - {Address (str) : response (List[str]) , ... } with type of Dict[str, List[str]]. + {Address (bytes) : response (List[bytes]) , ... } with type of Dict[bytes, List[bytes]]. Examples: >>> await client.time() - ['1710925775', '913580'] + [b'1710925775', b'913580'] >>> await client.time(AllNodes()) - {'addr': ['1710925775', '913580'], 'addr2': ['1710925775', '913580'], 'addr3': ['1710925775', '913580']} + {b'addr': [b'1710925775', b'913580'], b'addr2': [b'1710925775', b'913580'], b'addr3': [b'1710925775', b'913580']} """ return cast( - TClusterResponse[List[str]], + TClusterResponse[List[bytes]], await self._execute_command(RequestType.Time, [], route), ) + + async def lastsave(self, route: Optional[Route] = None) -> TClusterResponse[int]: + """ + Returns the Unix time of the last DB save timestamp or startup timestamp if no save was made since then. + + See https://valkey.io/commands/lastsave for more details. + + Args: + route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TClusterResponse[int]: The Unix time of the last successful DB save. + If no route is provided, or a single node route is requested, returns an int representing the Unix time + of the last successful DB save. Otherwise, returns a dict of [bytes , int] where each key contains the + address of the queried node and the value contains the Unix time of the last successful DB save. + + Examples: + >>> await client.lastsave() + 1710925775 # Unix time of the last DB save + >>> await client.lastsave(AllNodes()) + {b'addr1': 1710925775, b'addr2': 1710925775, b'addr3': 1710925775} # Unix time of the last DB save on each node + """ + return cast( + TClusterResponse[int], + await self._execute_command(RequestType.LastSave, [], route), + ) + + async def sort( + self, + key: TEncodable, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> List[bytes]: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + This command is routed to primary nodes only. + To store the result into a new key, see `sort_store`. + + By default, sorting is numeric, and elements are compared by their value interpreted as double precision floating point numbers. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Returns: + List[bytes]: A list of sorted elements. + + Examples: + >>> await client.lpush(b"mylist", [b'3', b'1', b'2']) + >>> await client.sort("mylist") + [b'1', b'2', b'3'] + + >>> await client.sort("mylist", order=OrderBy.DESC) + ['3', '2', '1'] + + >>> await client.lpush(b"mylist", [b'2', b'1', b'2', b'3', b'3', b'1']) + >>> await client.sort("mylist", limit=Limit(2, 3)) + [b'2', b'2', b'3'] + + >>> await client.lpush(b"mylist", [b"a", b"b", b"c", b"d"]) + >>> await client.sort(b"mylist", limit=Limit(2, 2), order=OrderBy.DESC, alpha=True) + [b'b', b'a'] + """ + args = _build_sort_args(key, None, limit, None, order, alpha) + result = await self._execute_command(RequestType.Sort, args) + return cast(List[bytes], result) + + async def sort_ro( + self, + key: TEncodable, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> List[bytes]: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort_ro` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + This command is routed depending on the client's `ReadFrom` strategy. + + By default, sorting is numeric, and elements are compared by their value interpreted as double precision floating point numbers. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Returns: + List[bytes]: A list of sorted elements. + + Examples: + >>> await client.lpush("mylist", '3', '1', '2') + >>> await client.sort_ro("mylist") + [b'1', b'2', b'3'] + + >>> await client.sort_ro("mylist", order=OrderBy.DESC) + [b'3', b'2', b'1'] + + >>> await client.lpush("mylist", '2', '1', '2', '3', '3', '1') + >>> await client.sort_ro("mylist", limit=Limit(2, 3)) + [b'1', b'2', b'2'] + + >>> await client.lpush("mylist", "a", "b", "c", "d") + >>> await client.sort_ro("mylist", limit=Limit(2, 2), order=OrderBy.DESC, alpha=True) + [b'b', b'a'] + + Since: Valkey version 7.0.0. + """ + args = _build_sort_args(key, None, limit, None, order, alpha) + result = await self._execute_command(RequestType.SortReadOnly, args) + return cast(List[bytes], result) + + async def sort_store( + self, + key: TEncodable, + destination: TEncodable, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> int: + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + When in cluster mode, `key` and `store` must map to the same hash slot. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + destination (TEncodable): The key where the sorted result will be stored. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Returns: + int: The number of elements in the sorted key stored at `store`. + + Examples: + >>> await client.lpush(b"mylist", [b'3', b'1', b'2']) + >>> await client.sort_store("mylist", b"sorted_list") + 3 # Indicates that the sorted list "sorted_list" contains three elements. + >>> await client.lrange("sorted_list", 0, -1) + [b'1', b'2', b'3'] + """ + args = _build_sort_args(key, None, limit, None, order, alpha, store=destination) + result = await self._execute_command(RequestType.Sort, args) + return cast(int, result) + + async def publish( + self, + message: TEncodable, + channel: TEncodable, + sharded: bool = False, + ) -> int: + """ + Publish a message on pubsub channel. + This command aggregates PUBLISH and SPUBLISH commands functionalities. + The mode is selected using the 'sharded' parameter. + For both sharded and non-sharded mode, request is routed using hashed channel as key. + See https://valkey.io/commands/publish and https://valkey.io/commands/spublish for more details. + + Args: + message (TEncodable): Message to publish. + channel (TEncodable): Channel to publish the message on. + sharded (bool): Use sharded pubsub mode. Available since Valkey version 7.0. + + Returns: + int: Number of subscriptions in that node that received the message. + + Examples: + >>> await client.publish("Hi all!", "global-channel", False) + 1 # Published 1 instance of "Hi all!" message on global-channel channel using non-sharded mode + >>> await client.publish(b"Hi to sharded channel1!", b"channel1", True) + 2 # Published 2 instances of "Hi to sharded channel1!" message on channel1 using sharded mode + """ + result = await self._execute_command( + RequestType.SPublish if sharded else RequestType.Publish, [channel, message] + ) + return cast(int, result) + + async def flushall( + self, flush_mode: Optional[FlushMode] = None, route: Optional[Route] = None + ) -> TOK: + """ + Deletes all the keys of all the existing databases. This command never fails. + + See https://valkey.io/commands/flushall for more details. + + Args: + flush_mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TOK: A simple OK response. + + Examples: + >>> await client.flushall(FlushMode.ASYNC) + OK # This command never fails. + >>> await client.flushall(FlushMode.ASYNC, AllNodes()) + OK # This command never fails. + """ + args: List[TEncodable] = [] + if flush_mode is not None: + args.append(flush_mode.value) + + return cast( + TOK, + await self._execute_command(RequestType.FlushAll, args, route), + ) + + async def flushdb( + self, flush_mode: Optional[FlushMode] = None, route: Optional[Route] = None + ) -> TOK: + """ + Deletes all the keys of the currently selected database. This command never fails. + + See https://valkey.io/commands/flushdb for more details. + + Args: + flush_mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TOK: A simple OK response. + + Examples: + >>> await client.flushdb() + OK # The keys of the currently selected database were deleted. + >>> await client.flushdb(FlushMode.ASYNC) + OK # The keys of the currently selected database were deleted asynchronously. + >>> await client.flushdb(FlushMode.ASYNC, AllNodes()) + OK # The keys of the currently selected database were deleted asynchronously on all nodes. + """ + args: List[TEncodable] = [] + if flush_mode is not None: + args.append(flush_mode.value) + + return cast( + TOK, + await self._execute_command(RequestType.FlushDB, args, route), + ) + + async def copy( + self, + source: TEncodable, + destination: TEncodable, + replace: Optional[bool] = None, + ) -> bool: + """ + Copies the value stored at the `source` to the `destination` key. When `replace` is True, + removes the `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Note: + Both `source` and `destination` must map to the same hash slot. + + Args: + source (TEncodable): The key to the source value. + destination (TEncodable): The key where the value should be copied to. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Returns: + bool: True if the source was copied. Otherwise, returns False. + + Examples: + >>> await client.set("source", "sheep") + >>> await client.copy(b"source", b"destination") + True # Source was copied + >>> await client.get("destination") + b"sheep" + + Since: Valkey version 6.2.0. + """ + args: List[TEncodable] = [source, destination] + if replace is True: + args.append("REPLACE") + return cast( + bool, + await self._execute_command(RequestType.Copy, args), + ) + + async def lolwut( + self, + version: Optional[int] = None, + parameters: Optional[List[int]] = None, + route: Optional[Route] = None, + ) -> TClusterResponse[bytes]: + """ + Displays a piece of generative computer art and the Valkey version. + + See https://valkey.io/commands/lolwut for more details. + + Args: + version (Optional[int]): Version of computer art to generate. + parameters (Optional[List[int]]): Additional set of arguments in order to change the output: + For version `5`, those are length of the line, number of squares per row, and number of squares per column. + For version `6`, those are number of columns and number of lines. + route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TClusterResponse[bytes]: A piece of generative computer art along with the current Valkey version. + When specifying a route other than a single node, response will be: + {Address (bytes) : response (bytes) , ... } with type of Dict[bytes, bytes]. + + Examples: + >>> await client.lolwut(6, [40, 20], RandomNode()); + b"Redis ver. 7.2.3" # Indicates the current Valkey version + """ + args: List[TEncodable] = [] + if version is not None: + args.extend(["VERSION", str(version)]) + if parameters: + for var in parameters: + args.extend(str(var)) + return cast( + TClusterResponse[bytes], + await self._execute_command(RequestType.Lolwut, args, route), + ) + + async def random_key(self, route: Optional[Route] = None) -> Optional[bytes]: + """ + Returns a random existing key name. + + See https://valkey.io/commands/randomkey for more details. + + Args: + route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + Optional[bytes]: A random existing key name. + + Examples: + >>> await client.random_key() + b"random_key_name" # "random_key_name" is a random existing key name. + """ + return cast( + Optional[bytes], + await self._execute_command(RequestType.RandomKey, [], route), + ) + + async def wait( + self, + numreplicas: int, + timeout: int, + route: Optional[Route] = None, + ) -> int: + """ + Blocks the current client until all the previous write commands are successfully transferred + and acknowledged by at least `numreplicas` of replicas. If `timeout` is + reached, the command returns even if the specified number of replicas were not yet reached. + + See https://valkey.io/commands/wait for more details. + + Args: + numreplicas (int): The number of replicas to reach. + timeout (int): The timeout value specified in milliseconds. A value of 0 will block indefinitely. + route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + int: The number of replicas reached by all the writes performed in the context of the current connection. + + Examples: + >>> await client.set("key", "value"); + >>> await client.wait(1, 1000); + // return 1 when a replica is reached or 0 if 1000ms is reached. + """ + args: List[TEncodable] = [str(numreplicas), str(timeout)] + return cast( + int, + await self._execute_command(RequestType.Wait, args, route), + ) + + async def unwatch(self, route: Optional[Route] = None) -> TOK: + """ + Flushes all the previously watched keys for a transaction. Executing a transaction will + automatically flush all previously watched keys. + + See https://valkey.io/commands/unwatch for more details. + + Args: + route (Optional[Route]): The command will be routed to all primary nodes, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TOK: A simple "OK" response. + + Examples: + >>> await client.unwatch() + 'OK' + """ + return cast( + TOK, + await self._execute_command(RequestType.UnWatch, [], route), + ) + + async def scan( + self, + cursor: ClusterScanCursor, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + type: Optional[ObjectType] = None, + ) -> List[Union[ClusterScanCursor, List[bytes]]]: + """ + Incrementally iterates over the keys in the Cluster. + The method returns a list containing the next cursor and a list of keys. + + This command is similar to the SCAN command, but it is designed to work in a Cluster environment. + For each iteration the new cursor object should be used to continue the scan. + Using the same cursor object for multiple iterations will result in the same keys or unexpected behavior. + For more information about the Cluster Scan implementation, + see [Cluster Scan](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#cluster-scan). + + As the SCAN command, the method can be used to iterate over the keys in the database, + to return all keys the database have from the time the scan started till the scan ends. + The same key can be returned in multiple scans iteration. + + See https://valkey.io/commands/scan/ for more details. + + Args: + cursor (ClusterScanCursor): The cursor object that wraps the scan state. + To start a new scan, create a new empty ClusterScanCursor using ClusterScanCursor(). + match (Optional[TEncodable]): A pattern to match keys against. + count (Optional[int]): The number of keys to return in a single iteration. + The actual number returned can vary and is not guaranteed to match this count exactly. + This parameter serves as a hint to the server on the number of steps to perform in each iteration. + The default value is 10. + type (Optional[ObjectType]): The type of object to scan for. + + Returns: + List[Union[ClusterScanCursor, List[TEncodable]]]: A list containing the next cursor and a list of keys, + formatted as [ClusterScanCursor, [key1, key2, ...]]. + + Examples: + >>> # In the following example, we will iterate over the keys in the cluster. + await client.mset({b'key1': b'value1', b'key2': b'value2', b'key3': b'value3'}) + cursor = ClusterScanCursor() + all_keys = [] + while not cursor.is_finished(): + cursor, keys = await client.scan(cursor, count=10) + all_keys.extend(keys) + print(all_keys) # [b'key1', b'key2', b'key3'] + >>> # In the following example, we will iterate over the keys in the cluster that match the pattern "*key*". + await client.mset({b"key1": b"value1", b"key2": b"value2", b"not_my_key": b"value3", b"something_else": b"value4"}) + cursor = ClusterScanCursor() + all_keys = [] + while not cursor.is_finished(): + cursor, keys = await client.scan(cursor, match=b"*key*", count=10) + all_keys.extend(keys) + print(all_keys) # [b'my_key1', b'my_key2', b'not_my_key'] + >>> # In the following example, we will iterate over the keys in the cluster that are of type STRING. + await client.mset({b'key1': b'value1', b'key2': b'value2', b'key3': b'value3'}) + await client.sadd(b"this_is_a_set", [b"value4"]) + cursor = ClusterScanCursor() + all_keys = [] + while not cursor.is_finished(): + cursor, keys = await client.scan(cursor, type=ObjectType.STRING) + all_keys.extend(keys) + print(all_keys) # [b'key1', b'key2', b'key3'] + """ + return cast( + List[Union[ClusterScanCursor, List[bytes]]], + await self._cluster_scan(cursor, match, count, type), + ) diff --git a/python/python/glide/async_commands/command_args.py b/python/python/glide/async_commands/command_args.py new file mode 100644 index 0000000000..92e0100665 --- /dev/null +++ b/python/python/glide/async_commands/command_args.py @@ -0,0 +1,101 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +from enum import Enum +from typing import List, Optional, Union + + +class Limit: + """ + Represents a limit argument for range queries in various commands. + + The `LIMIT` argument is commonly used to specify a subset of results from the matching elements, + similar to the `LIMIT` clause in SQL (e.g., `SELECT LIMIT offset, count`). + + This class can be utilized in multiple commands that support limit options, + such as [ZRANGE](https://valkey.io/commands/zrange), [SORT](https://valkey.io/commands/sort/), and others. + + Args: + offset (int): The starting position of the range, zero based. + count (int): The maximum number of elements to include in the range. + A negative count returns all elements from the offset. + + Examples: + >>> limit = Limit(0, 10) # Fetch the first 10 elements + >>> limit = Limit(5, -1) # Fetch all elements starting from the 5th element + """ + + def __init__(self, offset: int, count: int): + self.offset = offset + self.count = count + + +class OrderBy(Enum): + """ + Enumeration representing sorting order options. + + This enum is used for the following commands: + - `SORT`: General sorting in ascending or descending order. + - `GEOSEARCH`: Sorting items based on their proximity to a center point. + """ + + ASC = "ASC" + """ + ASC: Sort in ascending order. + """ + + DESC = "DESC" + """ + DESC: Sort in descending order. + """ + + +class ListDirection(Enum): + """ + Enumeration representing element popping or adding direction for List commands. + """ + + LEFT = "LEFT" + """ + LEFT: Represents the option that elements should be popped from or added to the left side of a list. + """ + + RIGHT = "RIGHT" + """ + RIGHT: Represents the option that elements should be popped from or added to the right side of a list. + """ + + +class ObjectType(Enum): + """ + Enumeration representing the data types supported by the database. + """ + + STRING = "String" + """ + Represents a string data type. + """ + + LIST = "List" + """ + Represents a list data type. + """ + + SET = "Set" + """ + Represents a set data type. + """ + + ZSET = "ZSet" + """ + Represents a sorted set data type. + """ + + HASH = "Hash" + """ + Represents a hash data type. + """ + + STREAM = "Stream" + """ + Represents a stream data type. + """ diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 4554dfabb9..3589f8d340 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -1,5 +1,5 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum from typing import ( @@ -16,27 +16,63 @@ get_args, ) +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldSubCommands, + BitmapIndexType, + BitwiseOperation, + OffsetOptions, + _create_bitfield_args, + _create_bitfield_read_only_args, +) +from glide.async_commands.command_args import Limit, ListDirection, ObjectType, OrderBy from glide.async_commands.sorted_set import ( + AggregationType, + GeoSearchByBox, + GeoSearchByRadius, + GeoSearchCount, + GeospatialData, + GeoUnit, InfBound, LexBoundary, RangeByIndex, RangeByLex, RangeByScore, ScoreBoundary, + ScoreFilter, + _create_geosearch_args, + _create_zinter_zunion_cmd_args, _create_zrange_args, ) -from glide.constants import TOK, TResult -from glide.protobuf.redis_request_pb2 import RequestType +from glide.async_commands.stream import ( + StreamAddOptions, + StreamClaimOptions, + StreamGroupOptions, + StreamPendingOptions, + StreamRangeBound, + StreamReadGroupOptions, + StreamReadOptions, + StreamTrimOptions, + _create_xpending_range_args, +) +from glide.constants import ( + TOK, + TEncodable, + TResult, + TXInfoStreamFullResponse, + TXInfoStreamResponse, +) +from glide.protobuf.command_request_pb2 import RequestType from glide.routes import Route -from ..glide import Script +from ..glide import ClusterScanCursor, Script class ConditionalChange(Enum): """ A condition to the `SET`, `ZADD` and `GEOADD` commands. - - ONLY_IF_EXISTS - Only update key / elements that already exist. Equivalent to `XX` in the Redis API - - ONLY_IF_DOES_NOT_EXIST - Only set key / add elements that does not already exist. Equivalent to `NX` in the Redis API + - ONLY_IF_EXISTS - Only update key / elements that already exist. Equivalent to `XX` in the Valkey API. + - ONLY_IF_DOES_NOT_EXIST - Only set key / add elements that does not already exist. Equivalent to `NX` in the Valkey API. """ ONLY_IF_EXISTS = "XX" @@ -45,39 +81,56 @@ class ConditionalChange(Enum): class ExpiryType(Enum): """SET option: The type of the expiry. - - SEC - Set the specified expire time, in seconds. Equivalent to `EX` in the Redis API. - - MILLSEC - Set the specified expire time, in milliseconds. Equivalent to `PX` in the Redis API. - - UNIX_SEC - Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Redis API. + - SEC - Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. + - MILLSEC - Set the specified expire time, in milliseconds. Equivalent to `PX` in the Valkey API. + - UNIX_SEC - Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Valkey API. + - UNIX_MILLSEC - Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the + Valkey API. + - KEEP_TTL - Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Valkey API. + """ + + SEC = 0, Union[int, timedelta] # Equivalent to `EX` in the Valkey API + MILLSEC = 1, Union[int, timedelta] # Equivalent to `PX` in the Valkey API + UNIX_SEC = 2, Union[int, datetime] # Equivalent to `EXAT` in the Valkey API + UNIX_MILLSEC = 3, Union[int, datetime] # Equivalent to `PXAT` in the Valkey API + KEEP_TTL = 4, Type[None] # Equivalent to `KEEPTTL` in the Valkey API + + +class ExpiryTypeGetEx(Enum): + """GetEx option: The type of the expiry. + - EX - Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. + - PX - Set the specified expire time, in milliseconds. Equivalent to `PX` in the Valkey API. + - UNIX_SEC - Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Valkey API. - UNIX_MILLSEC - Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the - Redis API. - - KEEP_TTL - Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Redis API. + Valkey API. + - PERSIST - Remove the time to live associated with the key. Equivalent to `PERSIST` in the Valkey API. """ - SEC = 0, Union[int, timedelta] # Equivalent to `EX` in the Redis API - MILLSEC = 1, Union[int, timedelta] # Equivalent to `PX` in the Redis API - UNIX_SEC = 2, Union[int, datetime] # Equivalent to `EXAT` in the Redis API - UNIX_MILLSEC = 3, Union[int, datetime] # Equivalent to `PXAT` in the Redis API - KEEP_TTL = 4, Type[None] # Equivalent to `KEEPTTL` in the Redis API + SEC = 0, Union[int, timedelta] # Equivalent to `EX` in the Valkey API + MILLSEC = 1, Union[int, timedelta] # Equivalent to `PX` in the Valkey API + UNIX_SEC = 2, Union[int, datetime] # Equivalent to `EXAT` in the Valkey API + UNIX_MILLSEC = 3, Union[int, datetime] # Equivalent to `PXAT` in the Valkey API + PERSIST = 4, Type[None] # Equivalent to `PERSIST` in the Valkey API class InfoSection(Enum): """ INFO option: a specific section of information: - -SERVER: General information about the Redis server + -SERVER: General information about the server -CLIENTS: Client connections section -MEMORY: Memory consumption related information -PERSISTENCE: RDB and AOF related information -STATS: General statistics -REPLICATION: Master/replica replication information -CPU: CPU consumption statistics - -COMMANDSTATS: Redis command statistics - -LATENCYSTATS: Redis command latency percentile distribution statistics - -SENTINEL: Redis Sentinel section (only applicable to Sentinel instances) - -CLUSTER: Redis Cluster section + -COMMANDSTATS: Valkey command statistics + -LATENCYSTATS: Valkey command latency percentile distribution statistics + -SENTINEL: Valkey Sentinel section (only applicable to Sentinel instances) + -CLUSTER: Valkey Cluster section -MODULES: Modules section -KEYSPACE: Database related statistics - -ERRORSTATS: Redis error statistics + -ERRORSTATS: Valkey error statistics -ALL: Return all sections (excluding module generated ones) -DEFAULT: Return only the default set of sections -EVERYTHING: Includes all and modules @@ -107,11 +160,11 @@ class ExpireOptions(Enum): """ EXPIRE option: options for setting key expiry. - - HasNoExpiry: Set expiry only when the key has no expiry (Equivalent to "NX" in Redis). - - HasExistingExpiry: Set expiry only when the key has an existing expiry (Equivalent to "XX" in Redis). + - HasNoExpiry: Set expiry only when the key has no expiry (Equivalent to "NX" in Valkey). + - HasExistingExpiry: Set expiry only when the key has an existing expiry (Equivalent to "XX" in Valkey). - NewExpiryGreaterThanCurrent: Set expiry only when the new expiry is greater than the current one (Equivalent - to "GT" in Redis). - - NewExpiryLessThanCurrent: Set expiry only when the new expiry is less than the current one (Equivalent to "LT" in Redis). + to "GT" in Valkey). + - NewExpiryLessThanCurrent: Set expiry only when the new expiry is less than the current one (Equivalent to "LT" in Valkey). """ HasNoExpiry = "NX" @@ -132,23 +185,6 @@ class UpdateOptions(Enum): GREATER_THAN = "GT" -class GeospatialData: - def __init__(self, longitude: float, latitude: float): - """ - Represents a geographic position defined by longitude and latitude. - - The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 are the following: - - Valid longitudes are from -180 to 180 degrees. - - Valid latitudes are from -85.05112878 to 85.05112878 degrees. - - Args: - longitude (float): The longitude coordinate. - latitude (float): The latitude coordinate. - """ - self.longitude = longitude - self.latitude = latitude - - class ExpirySet: """SET option: Represents the expiry type and value to be executed with "SET" command.""" @@ -202,71 +238,198 @@ def get_cmd_args(self) -> List[str]: return [self.cmd_arg] if self.value is None else [self.cmd_arg, self.value] +class ExpiryGetEx: + """GetEx option: Represents the expiry type and value to be executed with "GetEx" command.""" + + def __init__( + self, + expiry_type: ExpiryTypeGetEx, + value: Optional[Union[int, datetime, timedelta]], + ) -> None: + """ + Args: + - expiry_type (ExpiryType): The expiry type. + - value (Optional[Union[int, datetime, timedelta]]): The value of the expiration type. The type of expiration + determines the type of expiration value: + - SEC: Union[int, timedelta] + - MILLSEC: Union[int, timedelta] + - UNIX_SEC: Union[int, datetime] + - UNIX_MILLSEC: Union[int, datetime] + - PERSIST: Type[None] + """ + self.set_expiry_type_and_value(expiry_type, value) + + def set_expiry_type_and_value( + self, + expiry_type: ExpiryTypeGetEx, + value: Optional[Union[int, datetime, timedelta]], + ): + if not isinstance(value, get_args(expiry_type.value[1])): + raise ValueError( + f"The value of {expiry_type} should be of type {expiry_type.value[1]}" + ) + self.expiry_type = expiry_type + if self.expiry_type == ExpiryTypeGetEx.SEC: + self.cmd_arg = "EX" + if isinstance(value, timedelta): + value = int(value.total_seconds()) + elif self.expiry_type == ExpiryTypeGetEx.MILLSEC: + self.cmd_arg = "PX" + if isinstance(value, timedelta): + value = int(value.total_seconds() * 1000) + elif self.expiry_type == ExpiryTypeGetEx.UNIX_SEC: + self.cmd_arg = "EXAT" + if isinstance(value, datetime): + value = int(value.timestamp()) + elif self.expiry_type == ExpiryTypeGetEx.UNIX_MILLSEC: + self.cmd_arg = "PXAT" + if isinstance(value, datetime): + value = int(value.timestamp() * 1000) + elif self.expiry_type == ExpiryTypeGetEx.PERSIST: + self.cmd_arg = "PERSIST" + self.value = str(value) if value else None + + def get_cmd_args(self) -> List[str]: + return [self.cmd_arg] if self.value is None else [self.cmd_arg, self.value] + + class InsertPosition(Enum): BEFORE = "BEFORE" AFTER = "AFTER" +class FlushMode(Enum): + """ + Defines flushing mode for: + + `FLUSHALL` command and `FUNCTION FLUSH` command. + + See https://valkey.io/commands/flushall/ and https://valkey.io/commands/function-flush/ for details + + SYNC was introduced in version 6.2.0. + """ + + ASYNC = "ASYNC" + SYNC = "SYNC" + + +class FunctionRestorePolicy(Enum): + """ + Options for the FUNCTION RESTORE command. + + - APPEND: Appends the restored libraries to the existing libraries and aborts on collision. This is the + default policy. + - FLUSH: Deletes all existing libraries before restoring the payload. + - REPLACE: Appends the restored libraries to the existing libraries, replacing any existing ones in case + of name collisions. Note that this policy doesn't prevent function name collisions, only libraries. + """ + + APPEND = "APPEND" + FLUSH = "FLUSH" + REPLACE = "REPLACE" + + +def _build_sort_args( + key: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + store: Optional[TEncodable] = None, +) -> List[TEncodable]: + args = [key] + + if by_pattern: + args.extend(["BY", by_pattern]) + + if limit: + args.extend(["LIMIT", str(limit.offset), str(limit.count)]) + + if get_patterns: + for pattern in get_patterns: + args.extend(["GET", pattern]) + + if order: + args.append(order.value) + + if alpha: + args.append("ALPHA") + + if store: + args.extend(["STORE", store]) + + return args + + class CoreCommands(Protocol): async def _execute_command( self, request_type: RequestType.ValueType, - args: List[str], + args: List[TEncodable], route: Optional[Route] = ..., ) -> TResult: ... async def _execute_transaction( self, - commands: List[Tuple[RequestType.ValueType, List[str]]], + commands: List[Tuple[RequestType.ValueType, List[TEncodable]]], route: Optional[Route] = None, ) -> List[TResult]: ... async def _execute_script( self, hash: str, - keys: Optional[List[str]] = None, - args: Optional[List[str]] = None, + keys: Optional[List[TEncodable]] = None, + args: Optional[List[TEncodable]] = None, route: Optional[Route] = None, ) -> TResult: ... + async def _cluster_scan( + self, + cursor: ClusterScanCursor, + match: Optional[TEncodable] = ..., + count: Optional[int] = ..., + type: Optional[ObjectType] = ..., + ) -> TResult: ... + async def set( self, - key: str, - value: str, + key: TEncodable, + value: TEncodable, conditional_set: Optional[ConditionalChange] = None, expiry: Optional[ExpirySet] = None, return_old_value: bool = False, - ) -> Optional[str]: + ) -> Optional[bytes]: """ Set the given key with the given value. Return value is dependent on the passed options. - See https://redis.io/commands/set/ for more details. + See https://valkey.io/commands/set/ for more details. Args: - key (str): the key to store. - value (str): the value to store with the given key. + key (TEncodable): the key to store. + value (TEncodable): the value to store with the given key. conditional_set (Optional[ConditionalChange], optional): set the key only if the given condition is met. - Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None. + Equivalent to [`XX` | `NX`] in the Valkey API. Defaults to None. expiry (Optional[ExpirySet], optional): set expiriation to the given key. - Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Redis API. Defaults to None. - return_old_value (bool, optional): Return the old string stored at key, or None if key did not exist. + Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Valkey API. Defaults to None. + return_old_value (bool, optional): Return the old value stored at key, or None if key did not exist. An error is returned and SET aborted if the value stored at key is not a string. - Equivalent to `GET` in the Redis API. Defaults to False. + Equivalent to `GET` in the Valkey API. Defaults to False. Returns: - Optional[str]: + Optional[bytes]: If the value is successfully set, return OK. If value isn't set because of only_if_exists or only_if_does_not_exist conditions, return None. - If return_old_value is set, return the old value as a string. + If return_old_value is set, return the old value as a bytes string. Example: - >>> await client.set("key", "value") + >>> await client.set(b"key", b"value") 'OK' >>> await client.set("key", "new_value",conditional_set=ConditionalChange.ONLY_IF_EXISTS, expiry=Expiry(ExpiryType.SEC, 5)) 'OK' # Set "new_value" to "key" only if "key" already exists, and set the key expiration to 5 seconds. >>> await client.set("key", "value", conditional_set=ConditionalChange.ONLY_IF_DOES_NOT_EXIST,return_old_value=True) - 'new_value' # Returns the old value of "key". + b'new_value' # Returns the old value of "key". >>> await client.get("key") - 'new_value' # Value wasn't modified back to being "value" because of "NX" flag. + b'new_value' # Value wasn't modified back to being "value" because of "NX" flag. """ args = [key, value] if conditional_set: @@ -275,42 +438,99 @@ async def set( args.append("GET") if expiry is not None: args.extend(expiry.get_cmd_args()) - return cast( - Optional[str], await self._execute_command(RequestType.SetString, args) - ) + return cast(Optional[bytes], await self._execute_command(RequestType.Set, args)) - async def get(self, key: str) -> Optional[str]: + async def get(self, key: TEncodable) -> Optional[bytes]: """ Get the value associated with the given key, or null if no such value exists. - See https://redis.io/commands/get/ for details. + See https://valkey.io/commands/get/ for details. Args: - key (str): The key to retrieve from the database. + key (TEncodable): The key to retrieve from the database. Returns: - Optional[str]: If the key exists, returns the value of the key as a string. Otherwise, return None. + Optional[bytes]: If the key exists, returns the value of the key as a byte string. Otherwise, return None. Example: >>> await client.get("key") - 'value' + b'value' + """ + args: List[TEncodable] = [key] + return cast(Optional[bytes], await self._execute_command(RequestType.Get, args)) + + async def getdel(self, key: TEncodable) -> Optional[bytes]: + """ + Gets a value associated with the given string `key` and deletes the key. + + See https://valkey.io/commands/getdel for more details. + + Args: + key (TEncodable): The `key` to retrieve from the database. + + Returns: + Optional[bytes]: If `key` exists, returns the `value` of `key`. Otherwise, returns `None`. + + Examples: + >>> await client.set("key", "value") + >>> await client.getdel("key") + b'value' + >>> await client.getdel("key") + None + """ + return cast( + Optional[bytes], await self._execute_command(RequestType.GetDel, [key]) + ) + + async def getrange(self, key: TEncodable, start: int, end: int) -> bytes: + """ + Returns the substring of the value stored at `key`, determined by the offsets `start` and `end` (both are inclusive). + Negative offsets can be used in order to provide an offset starting from the end of the value. + So `-1` means the last character, `-2` the penultimate and so forth. + + If `key` does not exist, an empty byte string is returned. If `start` or `end` + are out of range, returns the substring within the valid range of the value. + + See https://valkey.io/commands/getrange/ for more details. + + Args: + key (TEncodable): The key of the string. + start (int): The starting offset. + end (int): The ending offset. + + Returns: + bytes: A substring extracted from the value stored at `key`. + + Examples: + >>> await client.set("mykey", "This is a string") + >>> await client.getrange("mykey", 0, 3) + b"This" + >>> await client.getrange("mykey", -3, -1) + b"ing" # extracted last 3 characters of a string + >>> await client.getrange("mykey", 0, 100) + b"This is a string" + >>> await client.getrange("non_existing", 5, 6) + b"" """ return cast( - Optional[str], await self._execute_command(RequestType.GetString, [key]) + bytes, + await self._execute_command( + RequestType.GetRange, [key, str(start), str(end)] + ), ) - async def append(self, key: str, value: str) -> int: + async def append(self, key: TEncodable, value: TEncodable) -> int: """ Appends a value to a key. If `key` does not exist it is created and set as an empty string, so `APPEND` will be similar to `SET` in this special case. - See https://redis.io/commands/append for more details. + See https://valkey.io/commands/append for more details. Args: - key (str): The key to which the value will be appended. - value (str): The value to append. + key (TEncodable): The key to which the value will be appended. + value (TEncodable): The value to append. Returns: - int: The length of the string after appending the value. + int: The length of the stored value after appending `value`. Examples: >>> await client.append("key", "Hello") @@ -318,17 +538,17 @@ async def append(self, key: str, value: str) -> int: >>> await client.append("key", " world") 11 # Indicates that " world" has been appended to the value of "key", resulting in a new value of "Hello world" with a length of 11. >>> await client.get("key") - "Hello world" # Returns the value stored in "key", which is now "Hello world". + b"Hello world" # Returns the value stored in "key", which is now "Hello world". """ return cast(int, await self._execute_command(RequestType.Append, [key, value])) - async def strlen(self, key: str) -> int: + async def strlen(self, key: TEncodable) -> int: """ Get the length of the string value stored at `key`. - See https://redis.io/commands/strlen/ for more details. + See https://valkey.io/commands/strlen/ for more details. Args: - key (str): The key to return its length. + key (TEncodable): The key to return its length. Returns: int: The length of the string value stored at `key`. @@ -339,19 +559,21 @@ async def strlen(self, key: str) -> int: >>> await client.strlen("key") 5 # Indicates that the length of the string value stored at `key` is 5. """ - return cast(int, await self._execute_command(RequestType.Strlen, [key])) + args: List[TEncodable] = [key] + return cast(int, await self._execute_command(RequestType.Strlen, args)) - async def rename(self, key: str, new_key: str) -> TOK: + async def rename(self, key: TEncodable, new_key: TEncodable) -> TOK: """ Renames `key` to `new_key`. If `newkey` already exists it is overwritten. - In Cluster mode, both `key` and `newkey` must be in the same hash slot, - meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. - See https://redis.io/commands/rename/ for more details. + See https://valkey.io/commands/rename/ for more details. + + Note: + When in cluster mode, both `key` and `newkey` must map to the same hash slot. Args: - key (str) : The key to rename. - new_key (str) : The new name of the key. + key (TEncodable) : The key to rename. + new_key (TEncodable) : The new name of the key. Returns: OK: If the `key` was successfully renamed, return "OK". If `key` does not exist, an error is thrown. @@ -360,13 +582,41 @@ async def rename(self, key: str, new_key: str) -> TOK: TOK, await self._execute_command(RequestType.Rename, [key, new_key]) ) - async def delete(self, keys: List[str]) -> int: + async def renamenx(self, key: TEncodable, new_key: TEncodable) -> bool: + """ + Renames `key` to `new_key` if `new_key` does not yet exist. + + See https://valkey.io/commands/renamenx for more details. + + Note: + When in cluster mode, both `key` and `new_key` must map to the same hash slot. + + Args: + key (TEncodable): The key to rename. + new_key (TEncodable): The new key name. + + Returns: + bool: True if `key` was renamed to `new_key`, or False if `new_key` already exists. + + Examples: + >>> await client.renamenx("old_key", "new_key") + True # "old_key" was renamed to "new_key" + """ + return cast( + bool, + await self._execute_command(RequestType.RenameNX, [key, new_key]), + ) + + async def delete(self, keys: List[TEncodable]) -> int: """ Delete one or more keys from the database. A key is ignored if it does not exist. - See https://redis.io/commands/del/ for details. + See https://valkey.io/commands/del/ for details. + + Note: + When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. Args: - keys (List[str]): A list of keys to be deleted from the database. + keys (List[TEncodable]): A list of keys to be deleted from the database. Returns: int: The number of keys that were deleted. @@ -380,14 +630,14 @@ async def delete(self, keys: List[str]) -> int: """ return cast(int, await self._execute_command(RequestType.Del, keys)) - async def incr(self, key: str) -> int: + async def incr(self, key: TEncodable) -> int: """ Increments the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incr/ for more details. + See https://valkey.io/commands/incr/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. Returns: int: The value of `key` after the increment. @@ -399,13 +649,13 @@ async def incr(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.Incr, [key])) - async def incrby(self, key: str, amount: int) -> int: + async def incrby(self, key: TEncodable, amount: int) -> int: """ Increments the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing - the operation. See https://redis.io/commands/incrby/ for more details. + the operation. See https://valkey.io/commands/incrby/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. amount (int) : The amount to increment. Returns: @@ -420,15 +670,15 @@ async def incrby(self, key: str, amount: int) -> int: int, await self._execute_command(RequestType.IncrBy, [key, str(amount)]) ) - async def incrbyfloat(self, key: str, amount: float) -> float: + async def incrbyfloat(self, key: TEncodable, amount: float) -> float: """ Increment the string representing a floating point number stored at `key` by `amount`. By using a negative increment value, the value stored at the `key` is decremented. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incrbyfloat/ for more details. + See https://valkey.io/commands/incrbyfloat/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. amount (float) : The amount to increment. Returns: @@ -444,13 +694,46 @@ async def incrbyfloat(self, key: str, amount: float) -> float: await self._execute_command(RequestType.IncrByFloat, [key, str(amount)]), ) - async def mset(self, key_value_map: Mapping[str, str]) -> TOK: + async def setrange(self, key: TEncodable, offset: int, value: TEncodable) -> int: + """ + Overwrites part of the string stored at `key`, starting at the specified + `offset`, for the entire length of `value`. + If the `offset` is larger than the current length of the string at `key`, + the string is padded with zero bytes to make `offset` fit. Creates the `key` + if it doesn't exist. + + See https://valkey.io/commands/setrange for more details. + + Args: + key (TEncodable): The key of the string to update. + offset (int): The position in the string where `value` should be written. + value (TEncodable): The value written with `offset`. + + Returns: + int: The length of the string stored at `key` after it was modified. + + Examples: + >>> await client.set("key", "Hello World") + >>> await client.setrange("key", 6, "Glide") + 11 # The length of the string stored at `key` after it was modified. + """ + return cast( + int, + await self._execute_command( + RequestType.SetRange, [key, str(offset), value] + ), + ) + + async def mset(self, key_value_map: Mapping[TEncodable, TEncodable]) -> TOK: """ Set multiple keys to multiple values in a single atomic operation. - See https://redis.io/commands/mset/ for more details. + See https://valkey.io/commands/mset/ for more details. + + Note: + When in cluster mode, the command may route to multiple nodes when keys in `key_value_map` map to different hash slots. Args: - parameters (Mapping[str, str]): A map of key value pairs. + key_value_map (Mapping[TEncodable, TEncodable]): A map of key value pairs. Returns: OK: a simple OK response. @@ -459,41 +742,74 @@ async def mset(self, key_value_map: Mapping[str, str]) -> TOK: >>> await client.mset({"key" : "value", "key2": "value2"}) 'OK' """ - parameters: List[str] = [] + parameters: List[TEncodable] = [] for pair in key_value_map.items(): parameters.extend(pair) return cast(TOK, await self._execute_command(RequestType.MSet, parameters)) - async def mget(self, keys: List[str]) -> List[Optional[str]]: + async def msetnx(self, key_value_map: Mapping[TEncodable, TEncodable]) -> bool: + """ + Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + more keys already exist, the entire operation fails. + + Note: + When in cluster mode, all keys in `key_value_map` must map to the same hash slot. + + See https://valkey.io/commands/msetnx/ for more details. + + Args: + key_value_map (Mapping[TEncodable, TEncodable]): A key-value map consisting of keys and their respective values to set. + + Returns: + bool: True if all keys were set. False if no key was set. + + Examples: + >>> await client.msetnx({"key1": "value1", "key2": "value2"}) + True + >>> await client.msetnx({"key2": "value4", "key3": "value5"}) + False + """ + parameters: List[TEncodable] = [] + for pair in key_value_map.items(): + parameters.extend(pair) + return cast( + bool, + await self._execute_command(RequestType.MSetNX, parameters), + ) + + async def mget(self, keys: List[TEncodable]) -> List[Optional[bytes]]: """ Retrieve the values of multiple keys. - See https://redis.io/commands/mget/ for more details. + See https://valkey.io/commands/mget/ for more details. + + Note: + When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. Args: - keys (List[str]): A list of keys to retrieve values for. + keys (List[TEncodable]): A list of keys to retrieve values for. Returns: - List[Optional[str]]: A list of values corresponding to the provided keys. If a key is not found, + List[Optional[bytes]]: A list of values corresponding to the provided keys. If a key is not found, its corresponding value in the list will be None. Examples: >>> await client.set("key1", "value1") >>> await client.set("key2", "value2") >>> await client.mget(["key1", "key2"]) - ['value1' , 'value2'] + [b'value1' , b'value2'] """ return cast( - List[Optional[str]], await self._execute_command(RequestType.MGet, keys) + List[Optional[bytes]], await self._execute_command(RequestType.MGet, keys) ) - async def decr(self, key: str) -> int: + async def decr(self, key: TEncodable) -> int: """ Decrement the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/decr/ for more details. + See https://valkey.io/commands/decr/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. Returns: int: The value of key after the decrement. @@ -505,14 +821,14 @@ async def decr(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.Decr, [key])) - async def decrby(self, key: str, amount: int) -> int: + async def decrby(self, key: TEncodable, amount: int) -> int: """ Decrements the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/decrby/ for more details. + See https://valkey.io/commands/decrby/ for more details. Args: - key (str): The key to decrement its value. + key (TEncodable): The key to decrement its value. amount (int) : The amount to decrement. Returns: @@ -527,14 +843,41 @@ async def decrby(self, key: str, amount: int) -> int: int, await self._execute_command(RequestType.DecrBy, [key, str(amount)]) ) - async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: + async def touch(self, keys: List[TEncodable]) -> int: + """ + Updates the last access time of specified keys. + + See https://valkey.io/commands/touch/ for details. + + Note: + When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + + Args: + keys (List[TEncodable]): The keys to update last access time. + + Returns: + int: The number of keys that were updated, a key is ignored if it doesn't exist. + + Examples: + >>> await client.set("myKey1", "value1") + >>> await client.set("myKey2", "value2") + >>> await client.touch(["myKey1", "myKey2", "nonExistentKey"]) + 2 # Last access time of 2 keys has been updated. + """ + return cast(int, await self._execute_command(RequestType.Touch, keys)) + + async def hset( + self, + key: TEncodable, + field_value_map: Mapping[TEncodable, TEncodable], + ) -> int: """ Sets the specified fields to their respective values in the hash stored at `key`. - See https://redis.io/commands/hset/ for more details. + See https://valkey.io/commands/hset/ for more details. Args: - key (str): The key of the hash. - field_value_map (Mapping[str, str]): A field-value map consisting of fields and their corresponding values + key (TEncodable): The key of the hash. + field_value_map (Mapping[TEncodable, TEncodable]): A field-value map consisting of fields and their corresponding values to be set in the hash stored at the specified key. Returns: @@ -544,55 +887,55 @@ async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: >>> await client.hset("my_hash", {"field": "value", "field2": "value2"}) 2 # Indicates that 2 fields were successfully set in the hash "my_hash". """ - field_value_list: List[str] = [key] + field_value_list: List[TEncodable] = [key] for pair in field_value_map.items(): field_value_list.extend(pair) return cast( int, - await self._execute_command(RequestType.HashSet, field_value_list), + await self._execute_command(RequestType.HSet, field_value_list), ) - async def hget(self, key: str, field: str) -> Optional[str]: + async def hget(self, key: TEncodable, field: TEncodable) -> Optional[bytes]: """ Retrieves the value associated with `field` in the hash stored at `key`. - See https://redis.io/commands/hget/ for more details. + See https://valkey.io/commands/hget/ for more details. Args: - key (str): The key of the hash. - field (str): The field whose value should be retrieved. + key (TEncodable): The key of the hash. + field (TEncodable): The field whose value should be retrieved. Returns: - Optional[str]: The value associated `field` in the hash. + Optional[bytes]: The value associated `field` in the hash. Returns None if `field` is not presented in the hash or `key` does not exist. Examples: - >>> await client.hset("my_hash", "field") + >>> await client.hset("my_hash", "field", "value") >>> await client.hget("my_hash", "field") - "value" + b"value" >>> await client.hget("my_hash", "nonexistent_field") None """ return cast( - Optional[str], - await self._execute_command(RequestType.HashGet, [key, field]), + Optional[bytes], + await self._execute_command(RequestType.HGet, [key, field]), ) async def hsetnx( self, - key: str, - field: str, - value: str, + key: TEncodable, + field: TEncodable, + value: TEncodable, ) -> bool: """ Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. If `key` does not exist, a new key holding a hash is created. If `field` already exists, this operation has no effect. - See https://redis.io/commands/hsetnx/ for more details. + See https://valkey.io/commands/hsetnx/ for more details. Args: - key (str): The key of the hash. - field (str): The field to set the value for. - value (str): The value to set. + key (TEncodable): The key of the hash. + field (TEncodable): The field to set the value for. + value (TEncodable): The value to set. Returns: bool: True if the field was set, False if the field already existed and was not set. @@ -608,16 +951,16 @@ async def hsetnx( await self._execute_command(RequestType.HSetNX, [key, field, value]), ) - async def hincrby(self, key: str, field: str, amount: int) -> int: + async def hincrby(self, key: TEncodable, field: TEncodable, amount: int) -> int: """ Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/hincrby/ for more details. + See https://valkey.io/commands/hincrby/ for more details. Args: - key (str): The key of the hash. - field (str): The field in the hash stored at `key` to increment or decrement its value. + key (TEncodable): The key of the hash. + field (TEncodable): The field in the hash stored at `key` to increment or decrement its value. amount (int): The amount by which to increment or decrement the field's value. Use a negative value to decrement. @@ -630,22 +973,22 @@ async def hincrby(self, key: str, field: str, amount: int) -> int: """ return cast( int, - await self._execute_command( - RequestType.HashIncrBy, [key, field, str(amount)] - ), + await self._execute_command(RequestType.HIncrBy, [key, field, str(amount)]), ) - async def hincrbyfloat(self, key: str, field: str, amount: float) -> float: + async def hincrbyfloat( + self, key: TEncodable, field: TEncodable, amount: float + ) -> float: """ Increment or decrement the floating-point value stored at `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/hincrbyfloat/ for more details. + See https://valkey.io/commands/hincrbyfloat/ for more details. Args: - key (str): The key of the hash. - field (str): The field in the hash stored at `key` to increment or decrement its value. + key (TEncodable): The key of the hash. + field (TEncodable): The field in the hash stored at `key` to increment or decrement its value. amount (float): The amount by which to increment or decrement the field's value. Use a negative value to decrement. @@ -659,18 +1002,18 @@ async def hincrbyfloat(self, key: str, field: str, amount: float) -> float: return cast( float, await self._execute_command( - RequestType.HashIncrByFloat, [key, field, str(amount)] + RequestType.HIncrByFloat, [key, field, str(amount)] ), ) - async def hexists(self, key: str, field: str) -> bool: + async def hexists(self, key: TEncodable, field: TEncodable) -> bool: """ Check if a field exists in the hash stored at `key`. - See https://redis.io/commands/hexists/ for more details. + See https://valkey.io/commands/hexists/ for more details. Args: - key (str): The key of the hash. - field (str): The field to check in the hash stored at `key`. + key (TEncodable): The key of the hash. + field (TEncodable): The field to check in the hash stored at `key`. Returns: bool: Returns 'True' if the hash contains the specified field. If the hash does not contain the field, @@ -683,61 +1026,63 @@ async def hexists(self, key: str, field: str) -> bool: False """ return cast( - bool, await self._execute_command(RequestType.HashExists, [key, field]) + bool, await self._execute_command(RequestType.HExists, [key, field]) ) - async def hgetall(self, key: str) -> Dict[str, str]: + async def hgetall(self, key: TEncodable) -> Dict[bytes, bytes]: """ Returns all fields and values of the hash stored at `key`. - See https://redis.io/commands/hgetall/ for details. + See https://valkey.io/commands/hgetall/ for details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Returns: - Dict[str, str]: A dictionary of fields and their values stored in the hash. Every field name in the list is followed by + Dict[bytes, bytes]: A dictionary of fields and their values stored in the hash. Every field name in the list is followed by its value. If `key` does not exist, it returns an empty dictionary. Examples: >>> await client.hgetall("my_hash") - {"field1": "value1", "field2": "value2"} + {b"field1": b"value1", b"field2": b"value2"} """ return cast( - Dict[str, str], await self._execute_command(RequestType.HashGetAll, [key]) + Dict[bytes, bytes], await self._execute_command(RequestType.HGetAll, [key]) ) - async def hmget(self, key: str, fields: List[str]) -> List[Optional[str]]: + async def hmget( + self, key: TEncodable, fields: List[TEncodable] + ) -> List[Optional[bytes]]: """ Retrieve the values associated with specified fields in the hash stored at `key`. - See https://redis.io/commands/hmget/ for details. + See https://valkey.io/commands/hmget/ for details. Args: - key (str): The key of the hash. - fields (List[str]): The list of fields in the hash stored at `key` to retrieve from the database. + key (TEncodable): The key of the hash. + fields (List[TEncodable]): The list of fields in the hash stored at `key` to retrieve from the database. Returns: - List[Optional[str]]: A list of values associated with the given fields, in the same order as they are requested. + List[Optional[bytes]]: A list of values associated with the given fields, in the same order as they are requested. For every field that does not exist in the hash, a null value is returned. If `key` does not exist, it is treated as an empty hash, and the function returns a list of null values. Examples: >>> await client.hmget("my_hash", ["field1", "field2"]) - ["value1", "value2"] # A list of values associated with the specified fields. + [b"value1", b"value2"] # A list of values associated with the specified fields. """ return cast( - List[Optional[str]], - await self._execute_command(RequestType.HashMGet, [key] + fields), + List[Optional[bytes]], + await self._execute_command(RequestType.HMGet, [key] + fields), ) - async def hdel(self, key: str, fields: List[str]) -> int: + async def hdel(self, key: TEncodable, fields: List[TEncodable]) -> int: """ Remove specified fields from the hash stored at `key`. - See https://redis.io/commands/hdel/ for more details. + See https://valkey.io/commands/hdel/ for more details. Args: - key (str): The key of the hash. - fields (List[str]): The list of fields to remove from the hash stored at `key`. + key (TEncodable): The key of the hash. + fields (List[TEncodable]): The list of fields to remove from the hash stored at `key`. Returns: int: The number of fields that were removed from the hash, excluding specified but non-existing fields. @@ -747,18 +1092,16 @@ async def hdel(self, key: str, fields: List[str]) -> int: >>> await client.hdel("my_hash", ["field1", "field2"]) 2 # Indicates that two fields were successfully removed from the hash. """ - return cast( - int, await self._execute_command(RequestType.HashDel, [key] + fields) - ) + return cast(int, await self._execute_command(RequestType.HDel, [key] + fields)) - async def hlen(self, key: str) -> int: + async def hlen(self, key: TEncodable) -> int: """ Returns the number of fields contained in the hash stored at `key`. - See https://redis.io/commands/hlen/ for more details. + See https://valkey.io/commands/hlen/ for more details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Returns: int: The number of fields in the hash, or 0 when the key does not exist. @@ -772,52 +1115,153 @@ async def hlen(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.HLen, [key])) - async def hvals(self, key: str) -> List[str]: + async def hvals(self, key: TEncodable) -> List[bytes]: """ Returns all values in the hash stored at `key`. - See https://redis.io/commands/hvals/ for more details. + See https://valkey.io/commands/hvals/ for more details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Returns: - List[str]: A list of values in the hash, or an empty list when the key does not exist. + List[bytes]: A list of values in the hash, or an empty list when the key does not exist. Examples: >>> await client.hvals("my_hash") - ["value1", "value2", "value3"] # Returns all the values stored in the hash "my_hash". + [b"value1", b"value2", b"value3"] # Returns all the values stored in the hash "my_hash". """ - return cast(List[str], await self._execute_command(RequestType.Hvals, [key])) + return cast(List[bytes], await self._execute_command(RequestType.HVals, [key])) - async def hkeys(self, key: str) -> List[str]: + async def hkeys(self, key: TEncodable) -> List[bytes]: """ Returns all field names in the hash stored at `key`. - See https://redis.io/commands/hkeys/ for more details. + See https://valkey.io/commands/hkeys/ for more details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Returns: - List[str]: A list of field names for the hash, or an empty list when the key does not exist. + List[bytes]: A list of field names for the hash, or an empty list when the key does not exist. Examples: >>> await client.hkeys("my_hash") - ["field1", "field2", "field3"] # Returns all the field names stored in the hash "my_hash". + [b"field1", b"field2", b"field3"] # Returns all the field names stored in the hash "my_hash". + """ + return cast(List[bytes], await self._execute_command(RequestType.HKeys, [key])) + + async def hrandfield(self, key: TEncodable) -> Optional[bytes]: + """ + Returns a random field name from the hash value stored at `key`. + + See https://valkey.io/commands/hrandfield for more details. + + Args: + key (TEncodable): The key of the hash. + + Returns: + Optional[bytes]: A random field name from the hash stored at `key`. + If the hash does not exist or is empty, None will be returned. + + Examples: + >>> await client.hrandfield("my_hash") + b"field1" # A random field name stored in the hash "my_hash". + """ + return cast( + Optional[bytes], await self._execute_command(RequestType.HRandField, [key]) + ) + + async def hrandfield_count(self, key: TEncodable, count: int) -> List[bytes]: + """ + Retrieves up to `count` random field names from the hash value stored at `key`. + + See https://valkey.io/commands/hrandfield for more details. + + Args: + key (TEncodable): The key of the hash. + count (int): The number of field names to return. + If `count` is positive, returns unique elements. + If `count` is negative, allows for duplicates elements. + + Returns: + List[bytes]: A list of random field names from the hash. + If the hash does not exist or is empty, the response will be an empty list. + + Examples: + >>> await client.hrandfield_count("my_hash", -3) + [b"field1", b"field1", b"field2"] # Non-distinct, random field names stored in the hash "my_hash". + >>> await client.hrandfield_count("non_existing_hash", 3) + [] # Empty list + """ + return cast( + List[bytes], + await self._execute_command(RequestType.HRandField, [key, str(count)]), + ) + + async def hrandfield_withvalues( + self, key: TEncodable, count: int + ) -> List[List[bytes]]: + """ + Retrieves up to `count` random field names along with their values from the hash value stored at `key`. + + See https://valkey.io/commands/hrandfield for more details. + + Args: + key (TEncodable): The key of the hash. + count (int): The number of field names to return. + If `count` is positive, returns unique elements. + If `count` is negative, allows for duplicates elements. + + Returns: + List[List[bytes]]: A list of `[field_name, value]` lists, where `field_name` is a random field name from the + hash and `value` is the associated value of the field name. + If the hash does not exist or is empty, the response will be an empty list. + + Examples: + >>> await client.hrandfield_withvalues("my_hash", -3) + [[b"field1", b"value1"], [b"field1", b"value1"], [b"field2", b"value2"]] + """ + return cast( + List[List[bytes]], + await self._execute_command( + RequestType.HRandField, [key, str(count), "WITHVALUES"] + ), + ) + + async def hstrlen(self, key: TEncodable, field: TEncodable) -> int: """ - return cast(List[str], await self._execute_command(RequestType.Hkeys, [key])) + Returns the string length of the value associated with `field` in the hash stored at `key`. - async def lpush(self, key: str, elements: List[str]) -> int: + See https://valkey.io/commands/hstrlen/ for more details. + + Args: + key (TEncodable): The key of the hash. + field (TEncodable): The field in the hash. + + Returns: + int: The string length or 0 if `field` or `key` does not exist. + + Examples: + >>> await client.hset("my_hash", "field", "value") + >>> await client.hstrlen("my_hash", "my_field") + 5 + """ + return cast( + int, + await self._execute_command(RequestType.HStrlen, [key, field]), + ) + + async def lpush(self, key: TEncodable, elements: List[TEncodable]) -> int: """ Insert all the specified values at the head of the list stored at `key`. `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. - See https://redis.io/commands/lpush/ for more details. + See https://valkey.io/commands/lpush/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the head of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the head of the list stored at `key`. Returns: int: The length of the list after the push operations. @@ -832,15 +1276,16 @@ async def lpush(self, key: str, elements: List[str]) -> int: int, await self._execute_command(RequestType.LPush, [key] + elements) ) - async def lpushx(self, key: str, elements: List[str]) -> int: + async def lpushx(self, key: TEncodable, elements: List[TEncodable]) -> int: """ - Inserts specified values at the head of the `list`, only if `key` already exists and holds a list. + Inserts all the specified values at the head of the list stored at `key`, only if `key` exists and holds a list. + If `key` is not a list, this performs no operation. - See https://redis.io/commands/lpushx/ for more details. + See https://valkey.io/commands/lpushx/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the head of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the head of the list stored at `key`. Returns: int: The length of the list after the push operation. @@ -855,131 +1300,267 @@ async def lpushx(self, key: str, elements: List[str]) -> int: int, await self._execute_command(RequestType.LPushX, [key] + elements) ) - async def lpop(self, key: str) -> Optional[str]: + async def lpop(self, key: TEncodable) -> Optional[bytes]: """ Remove and return the first elements of the list stored at `key`. The command pops a single element from the beginning of the list. - See https://redis.io/commands/lpop/ for details. + See https://valkey.io/commands/lpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. Returns: - Optional[str]: The value of the first element. + Optional[bytes]: The value of the first element. If `key` does not exist, None will be returned. Examples: >>> await client.lpop("my_list") - "value1" + b"value1" >>> await client.lpop("non_exiting_key") None """ return cast( - Optional[str], + Optional[bytes], await self._execute_command(RequestType.LPop, [key]), ) - async def lpop_count(self, key: str, count: int) -> Optional[List[str]]: + async def lpop_count(self, key: TEncodable, count: int) -> Optional[List[bytes]]: """ Remove and return up to `count` elements from the list stored at `key`, depending on the list's length. - See https://redis.io/commands/lpop/ for details. + See https://valkey.io/commands/lpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. count (int): The count of elements to pop from the list. Returns: - Optional[List[str]]: A a list of popped elements will be returned depending on the list's length. + Optional[List[bytes]]: A a list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. Examples: >>> await client.lpop_count("my_list", 2) - ["value1", "value2"] + [b"value1", b"value2"] >>> await client.lpop_count("non_exiting_key" , 3) None """ return cast( - Optional[List[str]], + Optional[List[bytes]], await self._execute_command(RequestType.LPop, [key, str(count)]), ) - async def lrange(self, key: str, start: int, end: int) -> List[str]: + async def blpop( + self, keys: List[TEncodable], timeout: float + ) -> Optional[List[bytes]]: """ - Retrieve the specified elements of the list stored at `key` within the given range. - The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next - element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, - with -1 being the last element of the list, -2 being the penultimate, and so on. - See https://redis.io/commands/lrange/ for details. + Pops an element from the head of the first list that is non-empty, with the given keys being checked in the + order that they are given. Blocks the connection when there are no elements to pop from any of the given lists. + See https://valkey.io/commands/blpop for details. + + Notes: + 1. When in cluster mode, all `keys` must map to the same hash slot. + 2. `BLPOP` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. Args: - key (str): The key of the list. - start (int): The starting point of the range. - end (int): The end of the range. + keys (List[TEncodable]): The keys of the lists to pop from. + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. Returns: - List[str]: A list of elements within the specified range. - If `start` exceeds the `end` of the list, or if `start` is greater than `end`, an empty list will be returned. - If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. - If `key` does not exist an empty list will be returned. + Optional[List[bytes]]: A two-element list containing the key from which the element was popped and the value of the + popped element, formatted as `[key, value]`. If no element could be popped and the `timeout` expired, returns None. Examples: - >>> await client.lrange("my_list", 0, 2) - ["value1", "value2", "value3"] - >>> await client.lrange("my_list", -2, -1) - ["value2", "value3"] - >>> await client.lrange("non_exiting_key", 0, 2) - [] + >>> await client.blpop(["list1", "list2"], 0.5) + [b"list1", b"element"] # "element" was popped from the head of the list with key "list1" """ return cast( - List[str], - await self._execute_command( - RequestType.LRange, [key, str(start), str(end)] - ), + Optional[List[bytes]], + await self._execute_command(RequestType.BLPop, keys + [str(timeout)]), ) - async def lindex( + async def lmpop( self, - key: str, - index: int, - ) -> Optional[str]: + keys: List[TEncodable], + direction: ListDirection, + count: Optional[int] = None, + ) -> Optional[Mapping[bytes, List[bytes]]]: """ - Returns the element at `index` in the list stored at `key`. + Pops one or more elements from the first non-empty list from the provided `keys`. + + When in cluster mode, all `keys` must map to the same hash slot. + + See https://valkey.io/commands/lmpop/ for details. + + Args: + keys (List[TEncodable]): An array of keys of lists. + direction (ListDirection): The direction based on which elements are popped from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + count (Optional[int]): The maximum number of popped elements. If not provided, defaults to popping a single element. + + Returns: + Optional[Mapping[bytes, List[bytes]]]: A map of `key` name mapped to an array of popped elements, or None if no elements could be popped. + + Examples: + >>> await client.lpush("testKey", ["one", "two", "three"]) + >>> await client.lmpop(["testKey"], ListDirection.LEFT, 2) + {b"testKey": [b"three", b"two"]} + + Since: Valkey version 7.0.0. + """ + args = [str(len(keys)), *keys, direction.value] + if count is not None: + args += ["COUNT", str(count)] + + return cast( + Optional[Mapping[bytes, List[bytes]]], + await self._execute_command(RequestType.LMPop, args), + ) + + async def blmpop( + self, + keys: List[TEncodable], + direction: ListDirection, + timeout: float, + count: Optional[int] = None, + ) -> Optional[Mapping[bytes, List[bytes]]]: + """ + Blocks the connection until it pops one or more elements from the first non-empty list from the provided `keys`. + + `BLMPOP` is the blocking variant of `LMPOP`. + + Notes: + 1. When in cluster mode, all `keys` must map to the same hash slot. + 2. `BLMPOP` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/blmpop/ for details. + + Args: + keys (List[TEncodable]): An array of keys of lists. + direction (ListDirection): The direction based on which elements are popped from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + count (Optional[int]): The maximum number of popped elements. If not provided, defaults to popping a single element. + + Returns: + Optional[Mapping[bytes, List[bytes]]]: A map of `key` name mapped to an array of popped elements, or None if no elements could be popped and the timeout expired. + + Examples: + >>> await client.lpush("testKey", ["one", "two", "three"]) + >>> await client.blmpop(["testKey"], ListDirection.LEFT, 0.1, 2) + {b"testKey": [b"three", b"two"]} + + Since: Valkey version 7.0.0. + """ + args = [str(timeout), str(len(keys)), *keys, direction.value] + if count is not None: + args += ["COUNT", str(count)] + + return cast( + Optional[Mapping[bytes, List[bytes]]], + await self._execute_command(RequestType.BLMPop, args), + ) + + async def lrange(self, key: TEncodable, start: int, end: int) -> List[bytes]: + """ + Retrieve the specified elements of the list stored at `key` within the given range. + The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next + element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, + with -1 being the last element of the list, -2 being the penultimate, and so on. + See https://valkey.io/commands/lrange/ for details. + + Args: + key (TEncodable): The key of the list. + start (int): The starting point of the range. + end (int): The end of the range. + + Returns: + List[bytes]: A list of elements within the specified range. + If `start` exceeds the `end` of the list, or if `start` is greater than `end`, an empty list will be returned. + If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. + If `key` does not exist an empty list will be returned. + + Examples: + >>> await client.lrange("my_list", 0, 2) + [b"value1", b"value2", b"value3"] + >>> await client.lrange("my_list", -2, -1) + [b"value2", b"value3"] + >>> await client.lrange("non_exiting_key", 0, 2) + [] + """ + return cast( + List[bytes], + await self._execute_command( + RequestType.LRange, [key, str(start), str(end)] + ), + ) + + async def lindex( + self, + key: TEncodable, + index: int, + ) -> Optional[bytes]: + """ + Returns the element at `index` in the list stored at `key`. The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to designate elements starting at the tail of the list. Here, -1 means the last element, -2 means the penultimate and so forth. - See https://redis.io/commands/lindex/ for more details. + See https://valkey.io/commands/lindex/ for more details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. index (int): The index of the element in the list to retrieve. Returns: - Optional[str]: The element at `index` in the list stored at `key`. + Optional[bytes]: The element at `index` in the list stored at `key`. If `index` is out of range or if `key` does not exist, None is returned. Examples: >>> await client.lindex("my_list", 0) - 'value1' # Returns the first element in the list stored at 'my_list'. + b'value1' # Returns the first element in the list stored at 'my_list'. >>> await client.lindex("my_list", -1) - 'value3' # Returns the last element in the list stored at 'my_list'. + b'value3' # Returns the last element in the list stored at 'my_list'. + """ + return cast( + Optional[bytes], + await self._execute_command(RequestType.LIndex, [key, str(index)]), + ) + + async def lset(self, key: TEncodable, index: int, element: TEncodable) -> TOK: + """ + Sets the list element at `index` to `element`. + + The index is zero-based, so `0` means the first element, `1` the second element and so on. + Negative indices can be used to designate elements starting at the tail of the list. + Here, `-1` means the last element, `-2` means the penultimate and so forth. + + See https://valkey.io/commands/lset/ for details. + + Args: + key (TEncodable): The key of the list. + index (int): The index of the element in the list to be set. + element (TEncodable): The new element to set at the specified index. + + Returns: + TOK: A simple `OK` response. + + Examples: + >>> await client.lset("testKey", 1, "two") + OK """ return cast( - Optional[str], - await self._execute_command(RequestType.Lindex, [key, str(index)]), + TOK, + await self._execute_command(RequestType.LSet, [key, str(index), element]), ) - async def rpush(self, key: str, elements: List[str]) -> int: + async def rpush(self, key: TEncodable, elements: List[TEncodable]) -> int: """ Inserts all the specified values at the tail of the list stored at `key`. `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. - See https://redis.io/commands/rpush/ for more details. + See https://valkey.io/commands/rpush/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the tail of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the tail of the list stored at `key`. Returns: int: The length of the list after the push operations. @@ -994,15 +1575,16 @@ async def rpush(self, key: str, elements: List[str]) -> int: int, await self._execute_command(RequestType.RPush, [key] + elements) ) - async def rpushx(self, key: str, elements: List[str]) -> int: + async def rpushx(self, key: TEncodable, elements: List[TEncodable]) -> int: """ - Inserts specified values at the tail of the `list`, only if `key` already exists and holds a list. + Inserts all the specified values at the tail of the list stored at `key`, only if `key` exists and holds a list. + If `key` is not a list, this performs no operation. - See https://redis.io/commands/rpushx/ for more details. + See https://valkey.io/commands/rpushx/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the tail of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the tail of the list stored at `key`. Returns: int: The length of the list after the push operation. @@ -1017,68 +1599,101 @@ async def rpushx(self, key: str, elements: List[str]) -> int: int, await self._execute_command(RequestType.RPushX, [key] + elements) ) - async def rpop(self, key: str, count: Optional[int] = None) -> Optional[str]: + async def rpop(self, key: TEncodable) -> Optional[bytes]: """ Removes and returns the last elements of the list stored at `key`. The command pops a single element from the end of the list. - See https://redis.io/commands/rpop/ for details. + See https://valkey.io/commands/rpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. Returns: - Optional[str]: The value of the last element. + Optional[bytes]: The value of the last element. If `key` does not exist, None will be returned. Examples: >>> await client.rpop("my_list") - "value1" + b"value1" >>> await client.rpop("non_exiting_key") None """ return cast( - Optional[str], + Optional[bytes], await self._execute_command(RequestType.RPop, [key]), ) - async def rpop_count(self, key: str, count: int) -> Optional[List[str]]: + async def rpop_count(self, key: TEncodable, count: int) -> Optional[List[bytes]]: """ Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. - See https://redis.io/commands/rpop/ for details. + See https://valkey.io/commands/rpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. count (int): The count of elements to pop from the list. Returns: - Optional[List[str]: A list of popped elements will be returned depending on the list's length. + Optional[List[bytes]: A list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. Examples: >>> await client.rpop_count("my_list", 2) - ["value1", "value2"] + [b"value1", b"value2"] >>> await client.rpop_count("non_exiting_key" , 7) None """ return cast( - Optional[List[str]], + Optional[List[bytes]], await self._execute_command(RequestType.RPop, [key, str(count)]), ) + async def brpop( + self, keys: List[TEncodable], timeout: float + ) -> Optional[List[bytes]]: + """ + Pops an element from the tail of the first list that is non-empty, with the given keys being checked in the + order that they are given. Blocks the connection when there are no elements to pop from any of the given lists. + See https://valkey.io/commands/brpop for details. + + Notes: + 1. When in cluster mode, all `keys` must map to the same hash slot. + 2. `BRPOP` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + Args: + keys (List[TEncodable]): The keys of the lists to pop from. + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. + + Returns: + Optional[List[bytes]]: A two-element list containing the key from which the element was popped and the value of the + popped element, formatted as `[key, value]`. If no element could be popped and the `timeout` expired, returns None. + + Examples: + >>> await client.brpop(["list1", "list2"], 0.5) + [b"list1", b"element"] # "element" was popped from the tail of the list with key "list1" + """ + return cast( + Optional[List[bytes]], + await self._execute_command(RequestType.BRPop, keys + [str(timeout)]), + ) + async def linsert( - self, key: str, position: InsertPosition, pivot: str, element: str + self, + key: TEncodable, + position: InsertPosition, + pivot: TEncodable, + element: TEncodable, ) -> int: """ Inserts `element` in the list at `key` either before or after the `pivot`. - See https://redis.io/commands/linsert/ for details. + See https://valkey.io/commands/linsert/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. position (InsertPosition): The relative position to insert into - either `InsertPosition.BEFORE` or `InsertPosition.AFTER` the `pivot`. - pivot (str): An element of the list. - element (str): The new element to insert. + pivot (TEncodable): An element of the list. + element (TEncodable): The new element to insert. Returns: int: The list length after a successful insert operation. @@ -1096,16 +1711,111 @@ async def linsert( ), ) - async def sadd(self, key: str, members: List[str]) -> int: + async def lmove( + self, + source: TEncodable, + destination: TEncodable, + where_from: ListDirection, + where_to: ListDirection, + ) -> Optional[bytes]: + """ + Atomically pops and removes the left/right-most element to the list stored at `source` + depending on `where_from`, and pushes the element at the first/last element of the list + stored at `destination` depending on `where_to`. + + When in cluster mode, both `source` and `destination` must map to the same hash slot. + + See https://valkey.io/commands/lmove/ for details. + + Args: + source (TEncodable): The key to the source list. + destination (TEncodable): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + + Returns: + Optional[bytes]: The popped element, or None if `source` does not exist. + + Examples: + >>> client.lpush("testKey1", ["two", "one"]) + >>> client.lpush("testKey2", ["four", "three"]) + >>> await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT) + b"one" + >>> updated_array1 = await client.lrange("testKey1", 0, -1) + [b"two"] + >>> await client.lrange("testKey2", 0, -1) + [b"one", b"three", b"four"] + + Since: Valkey version 6.2.0. + """ + return cast( + Optional[bytes], + await self._execute_command( + RequestType.LMove, + [source, destination, where_from.value, where_to.value], + ), + ) + + async def blmove( + self, + source: TEncodable, + destination: TEncodable, + where_from: ListDirection, + where_to: ListDirection, + timeout: float, + ) -> Optional[bytes]: + """ + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `BLMOVE` is the blocking variant of `LMOVE`. + + Notes: + 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. + 2. `BLMOVE` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/blmove/ for details. + + Args: + source (TEncodable): The key to the source list. + destination (TEncodable): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + + Returns: + Optional[bytes]: The popped element, or None if `source` does not exist or if the operation timed-out. + + Examples: + >>> await client.lpush("testKey1", ["two", "one"]) + >>> await client.lpush("testKey2", ["four", "three"]) + >>> await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1) + b"one" + >>> await client.lrange("testKey1", 0, -1) + [b"two"] + >>> updated_array2 = await client.lrange("testKey2", 0, -1) + [b"one", b"three", bb"four"] + + Since: Valkey version 6.2.0. + """ + return cast( + Optional[bytes], + await self._execute_command( + RequestType.BLMove, + [source, destination, where_from.value, where_to.value, str(timeout)], + ), + ) + + async def sadd(self, key: TEncodable, members: List[TEncodable]) -> int: """ Add specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. If `key` does not exist, a new set is created before adding `members`. - See https://redis.io/commands/sadd/ for more details. + See https://valkey.io/commands/sadd/ for more details. Args: - key (str): The key where members will be added to its set. - members (List[str]): A list of members to add to the set stored at `key`. + key (TEncodable): The key where members will be added to its set. + members (List[TEncodable]): A list of members to add to the set stored at `key`. Returns: int: The number of members that were added to the set, excluding members already present. @@ -1116,15 +1826,15 @@ async def sadd(self, key: str, members: List[str]) -> int: """ return cast(int, await self._execute_command(RequestType.SAdd, [key] + members)) - async def srem(self, key: str, members: List[str]) -> int: + async def srem(self, key: TEncodable, members: List[TEncodable]) -> int: """ Remove specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. - See https://redis.io/commands/srem/ for details. + See https://valkey.io/commands/srem/ for details. Args: - key (str): The key from which members will be removed. - members (List[str]): A list of members to remove from the set stored at `key`. + key (TEncodable): The key from which members will be removed. + members (List[TEncodable]): A list of members to remove from the set stored at `key`. Returns: int: The number of members that were removed from the set, excluding non-existing members. @@ -1136,31 +1846,33 @@ async def srem(self, key: str, members: List[str]) -> int: """ return cast(int, await self._execute_command(RequestType.SRem, [key] + members)) - async def smembers(self, key: str) -> Set[str]: + async def smembers(self, key: TEncodable) -> Set[bytes]: """ Retrieve all the members of the set value stored at `key`. - See https://redis.io/commands/smembers/ for details. + See https://valkey.io/commands/smembers/ for details. Args: - key (str): The key from which to retrieve the set members. + key (TEncodable): The key from which to retrieve the set members. Returns: - Set[str]: A set of all members of the set. - If `key` does not exist an empty list will be returned. + Set[bytes]: A set of all members of the set. + If `key` does not exist an empty set will be returned. Examples: >>> await client.smembers("my_set") - {"member1", "member2", "member3"} + {b"member1", b"member2", b"member3"} """ - return cast(Set[str], await self._execute_command(RequestType.SMembers, [key])) + return cast( + Set[bytes], await self._execute_command(RequestType.SMembers, [key]) + ) - async def scard(self, key: str) -> int: + async def scard(self, key: TEncodable) -> int: """ Retrieve the set cardinality (number of elements) of the set stored at `key`. - See https://redis.io/commands/scard/ for details. + See https://valkey.io/commands/scard/ for details. Args: - key (str): The key from which to retrieve the number of set members. + key (TEncodable): The key from which to retrieve the number of set members. Returns: int: The cardinality (number of elements) of the set, or 0 if the key does not exist. @@ -1171,7 +1883,7 @@ async def scard(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.SCard, [key])) - async def spop(self, key: str) -> Optional[str]: + async def spop(self, key: TEncodable) -> Optional[bytes]: """ Removes and returns one random member from the set stored at `key`. @@ -1179,21 +1891,23 @@ async def spop(self, key: str) -> Optional[str]: To pop multiple members, see `spop_count`. Args: - key (str): The key of the set. + key (TEncodable): The key of the set. Returns: - Optional[str]: The value of the popped member. + Optional[bytes]: The value of the popped member. If `key` does not exist, None will be returned. Examples: >>> await client.spop("my_set") - "value1" # Removes and returns a random member from the set "my_set". + b"value1" # Removes and returns a random member from the set "my_set". >>> await client.spop("non_exiting_key") None """ - return cast(Optional[str], await self._execute_command(RequestType.Spop, [key])) + return cast( + Optional[bytes], await self._execute_command(RequestType.SPop, [key]) + ) - async def spop_count(self, key: str, count: int) -> Set[str]: + async def spop_count(self, key: TEncodable, count: int) -> Set[bytes]: """ Removes and returns up to `count` random members from the set stored at `key`, depending on the set's length. @@ -1201,36 +1915,36 @@ async def spop_count(self, key: str, count: int) -> Set[str]: To pop a single member, see `spop`. Args: - key (str): The key of the set. + key (TEncodable): The key of the set. count (int): The count of the elements to pop from the set. Returns: - Set[str]: A set of popped elements will be returned depending on the set's length. - If `key` does not exist, an empty set will be returned. + Set[bytes]: A set of popped elements will be returned depending on the set's length. + If `key` does not exist, an empty set will be returned. Examples: >>> await client.spop_count("my_set", 2) - {"value1", "value2"} # Removes and returns 2 random members from the set "my_set". + {b"value1", b"value2"} # Removes and returns 2 random members from the set "my_set". >>> await client.spop_count("non_exiting_key", 2) Set() """ return cast( - Set[str], await self._execute_command(RequestType.Spop, [key, str(count)]) + Set[bytes], await self._execute_command(RequestType.SPop, [key, str(count)]) ) async def sismember( self, - key: str, - member: str, + key: TEncodable, + member: TEncodable, ) -> bool: """ Returns if `member` is a member of the set stored at `key`. - See https://redis.io/commands/sismember/ for more details. + See https://valkey.io/commands/sismember/ for more details. Args: - key (str): The key of the set. - member (str): The member to check for existence in the set. + key (TEncodable): The key of the set. + member (TEncodable): The member to check for existence in the set. Returns: bool: True if the member exists in the set, False otherwise. @@ -1247,17 +1961,278 @@ async def sismember( await self._execute_command(RequestType.SIsMember, [key, member]), ) - async def ltrim(self, key: str, start: int, end: int) -> TOK: + async def smove( + self, + source: TEncodable, + destination: TEncodable, + member: TEncodable, + ) -> bool: + """ + Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. Creates a + new destination set if needed. The operation is atomic. + + See https://valkey.io/commands/smove for more details. + + Note: + When in cluster mode, `source` and `destination` must map to the same hash slot. + + Args: + source (TEncodable): The key of the set to remove the element from. + destination (TEncodable): The key of the set to add the element to. + member (TEncodable): The set element to move. + + Returns: + bool: True on success, or False if the `source` set does not exist or the element is not a member of the source set. + + Examples: + >>> await client.smove("set1", "set2", "member1") + True # "member1" was moved from "set1" to "set2". + """ + return cast( + bool, + await self._execute_command( + RequestType.SMove, [source, destination, member] + ), + ) + + async def sunion(self, keys: List[TEncodable]) -> Set[bytes]: + """ + Gets the union of all the given sets. + + See https://valkey.io/commands/sunion for more details. + + Note: + When in cluster mode, all `keys` must map to the same hash slot. + + Args: + keys (List[TEncodable]): The keys of the sets. + + Returns: + Set[bytes]: A set of members which are present in at least one of the given sets. + If none of the sets exist, an empty set will be returned. + + Examples: + >>> await client.sadd("my_set1", ["member1", "member2"]) + >>> await client.sadd("my_set2", ["member2", "member3"]) + >>> await client.sunion(["my_set1", "my_set2"]) + {b"member1", b"member2", b"member3"} # sets "my_set1" and "my_set2" have three unique members + >>> await client.sunion(["my_set1", "non_existing_set"]) + {b"member1", b"member2"} + """ + return cast(Set[bytes], await self._execute_command(RequestType.SUnion, keys)) + + async def sunionstore( + self, + destination: TEncodable, + keys: List[TEncodable], + ) -> int: + """ + Stores the members of the union of all given sets specified by `keys` into a new set at `destination`. + + See https://valkey.io/commands/sunionstore for more details. + + Note: + When in cluster mode, all keys in `keys` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key of the destination set. + keys (List[TEncodable]): The keys from which to retrieve the set members. + + Returns: + int: The number of elements in the resulting set. + + Examples: + >>> await client.sadd("set1", ["member1"]) + >>> await client.sadd("set2", ["member2"]) + >>> await client.sunionstore("my_set", ["set1", "set2"]) + 2 # Two elements were stored in "my_set", and those two members are the union of "set1" and "set2". + """ + return cast( + int, + await self._execute_command(RequestType.SUnionStore, [destination] + keys), + ) + + async def sdiffstore(self, destination: TEncodable, keys: List[TEncodable]) -> int: + """ + Stores the difference between the first set and all the successive sets in `keys` into a new set at + `destination`. + + See https://valkey.io/commands/sdiffstore for more details. + + Note: + When in Cluster mode, all keys in `keys` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key of the destination set. + keys (List[TEncodable]): The keys of the sets to diff. + + Returns: + int: The number of elements in the resulting set. + + Examples: + >>> await client.sadd("set1", ["member1", "member2"]) + >>> await client.sadd("set2", ["member1"]) + >>> await client.sdiffstore("set3", ["set1", "set2"]) + 1 # Indicates that one member was stored in "set3", and that member is the diff between "set1" and "set2". + """ + return cast( + int, + await self._execute_command(RequestType.SDiffStore, [destination] + keys), + ) + + async def sinter(self, keys: List[TEncodable]) -> Set[bytes]: + """ + Gets the intersection of all the given sets. + + See https://valkey.io/commands/sinter for more details. + + Note: + When in cluster mode, all `keys` must map to the same hash slot. + + Args: + keys (List[TEncodable]): The keys of the sets. + + Returns: + Set[bytes]: A set of members which are present in all given sets. + If one or more sets do no exist, an empty set will be returned. + + Examples: + >>> await client.sadd("my_set1", ["member1", "member2"]) + >>> await client.sadd("my_set2", ["member2", "member3"]) + >>> await client.sinter(["my_set1", "my_set2"]) + {b"member2"} # sets "my_set1" and "my_set2" have one commom member + >>> await client.sinter([my_set1", "non_existing_set"]) + None + """ + return cast(Set[bytes], await self._execute_command(RequestType.SInter, keys)) + + async def sinterstore(self, destination: TEncodable, keys: List[TEncodable]) -> int: + """ + Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. + + See https://valkey.io/commands/sinterstore for more details. + + Note: + When in Cluster mode, all `keys` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key of the destination set. + keys (List[TEncodable]): The keys from which to retrieve the set members. + + Returns: + int: The number of elements in the resulting set. + + Examples: + >>> await client.sadd("my_set1", ["member1", "member2"]) + >>> await client.sadd("my_set2", ["member2", "member3"]) + >>> await client.sinterstore("my_set3", ["my_set1", "my_set2"]) + 1 # One element was stored at "my_set3", and that element is the intersection of "my_set1" and "myset2". + """ + return cast( + int, + await self._execute_command(RequestType.SInterStore, [destination] + keys), + ) + + async def sintercard( + self, keys: List[TEncodable], limit: Optional[int] = None + ) -> int: + """ + Gets the cardinality of the intersection of all the given sets. + Optionally, a `limit` can be specified to stop the computation early if the intersection cardinality reaches the specified limit. + + When in cluster mode, all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/sintercard for more details. + + Args: + keys (List[TEncodable]): A list of keys representing the sets to intersect. + limit (Optional[int]): An optional limit to the maximum number of intersecting elements to count. + If specified, the computation stops as soon as the cardinality reaches this limit. + + Returns: + int: The number of elements in the resulting set of the intersection. + + Examples: + >>> await client.sadd("set1", {"a", "b", "c"}) + >>> await client.sadd("set2", {"b", "c", "d"}) + >>> await client.sintercard(["set1", "set2"]) + 2 # The intersection of "set1" and "set2" contains 2 elements: "b" and "c". + + >>> await client.sintercard(["set1", "set2"], limit=1) + 1 # The computation stops early as the intersection cardinality reaches the limit of 1. + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + if limit is not None: + args += ["LIMIT", str(limit)] + return cast( + int, + await self._execute_command(RequestType.SInterCard, args), + ) + + async def sdiff(self, keys: List[TEncodable]) -> Set[bytes]: + """ + Computes the difference between the first set and all the successive sets in `keys`. + + See https://valkey.io/commands/sdiff for more details. + + Note: + When in cluster mode, all `keys` must map to the same hash slot. + + Args: + keys (List[TEncodable]): The keys of the sets to diff + + Returns: + Set[bytes]: A set of elements representing the difference between the sets. + If any of the keys in `keys` do not exist, they are treated as empty sets. + + Examples: + >>> await client.sadd("set1", ["member1", "member2"]) + >>> await client.sadd("set2", ["member1"]) + >>> await client.sdiff("set1", "set2") + {b"member2"} # "member2" is in "set1" but not "set2" + """ + return cast( + Set[bytes], + await self._execute_command(RequestType.SDiff, keys), + ) + + async def smismember( + self, key: TEncodable, members: List[TEncodable] + ) -> List[bool]: + """ + Checks whether each member is contained in the members of the set stored at `key`. + + See https://valkey.io/commands/smismember for more details. + + Args: + key (TEncodable): The key of the set to check. + members (List[TEncodable]): A list of members to check for existence in the set. + + Returns: + List[bool]: A list of bool values, each indicating if the respective member exists in the set. + + Examples: + >>> await client.sadd("set1", ["a", "b", "c"]) + >>> await client.smismember("set1", ["b", "c", "d"]) + [True, True, False] # "b" and "c" are members of "set1", but "d" is not. + """ + return cast( + List[bool], + await self._execute_command(RequestType.SMIsMember, [key] + members), + ) + + async def ltrim(self, key: TEncodable, start: int, end: int) -> TOK: """ Trim an existing list so that it will contain only the specified range of elements specified. The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last element of the list, -2 being the penultimate, and so on. - See https://redis.io/commands/ltrim/ for more details. + See https://valkey.io/commands/ltrim/ for more details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. start (int): The starting point of the range. end (int): The end of the range. @@ -1277,19 +2252,19 @@ async def ltrim(self, key: str, start: int, end: int) -> TOK: await self._execute_command(RequestType.LTrim, [key, str(start), str(end)]), ) - async def lrem(self, key: str, count: int, element: str) -> int: + async def lrem(self, key: TEncodable, count: int, element: TEncodable) -> int: """ Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. If `count` is positive, it removes elements equal to `element` moving from head to tail. If `count` is negative, it removes elements equal to `element` moving from tail to head. If `count` is 0 or greater than the occurrences of elements equal to `element`, it removes all elements equal to `element`. - See https://redis.io/commands/lrem/ for more details. + See https://valkey.io/commands/lrem/ for more details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. count (int): The count of occurrences of elements equal to `element` to remove. - element (str): The element to remove from the list. + element (TEncodable): The element to remove from the list. Returns: int: The number of removed elements. @@ -1304,13 +2279,13 @@ async def lrem(self, key: str, count: int, element: str) -> int: await self._execute_command(RequestType.LRem, [key, str(count), element]), ) - async def llen(self, key: str) -> int: + async def llen(self, key: TEncodable) -> int: """ Get the length of the list stored at `key`. - See https://redis.io/commands/llen/ for details. + See https://valkey.io/commands/llen/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. Returns: int: The length of the list at the specified key. @@ -1322,13 +2297,16 @@ async def llen(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.LLen, [key])) - async def exists(self, keys: List[str]) -> int: + async def exists(self, keys: List[TEncodable]) -> int: """ Returns the number of keys in `keys` that exist in the database. - See https://redis.io/commands/exists/ for more details. + See https://valkey.io/commands/exists/ for more details. + + Note: + When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. Args: - keys (List[str]): The list of keys to check. + keys (List[TEncodable]): The list of keys to check. Returns: int: The number of keys that exist. If the same existing key is mentioned in `keys` multiple times, @@ -1340,16 +2318,19 @@ async def exists(self, keys: List[str]) -> int: """ return cast(int, await self._execute_command(RequestType.Exists, keys)) - async def unlink(self, keys: List[str]) -> int: + async def unlink(self, keys: List[TEncodable]) -> int: """ Unlink (delete) multiple keys from the database. A key is ignored if it does not exist. This command, similar to DEL, removes specified keys and ignores non-existent ones. - However, this command does not block the server, while [DEL](https://redis.io/commands/del) does. - See https://redis.io/commands/unlink/ for more details. + However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. + See https://valkey.io/commands/unlink/ for more details. + + Note: + When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. Args: - keys (List[str]): The list of keys to unlink. + keys (List[TEncodable]): The list of keys to unlink. Returns: int: The number of keys that were unlinked. @@ -1361,17 +2342,20 @@ async def unlink(self, keys: List[str]) -> int: return cast(int, await self._execute_command(RequestType.Unlink, keys)) async def expire( - self, key: str, seconds: int, option: Optional[ExpireOptions] = None + self, + key: TEncodable, + seconds: int, + option: Optional[ExpireOptions] = None, ) -> bool: """ Sets a timeout on `key` in seconds. After the timeout has expired, the key will automatically be deleted. If `key` already has an existing expire set, the time to live is updated to the new value. If `seconds` is a non-positive number, the key will be deleted rather than expired. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/expire/ for more details. + See https://valkey.io/commands/expire/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. seconds (int): The timeout in seconds. option (ExpireOptions, optional): The expire option. @@ -1383,13 +2367,16 @@ async def expire( >>> await client.expire("my_key", 60) True # Indicates that a timeout of 60 seconds has been set for "my_key." """ - args: List[str] = ( + args: List[TEncodable] = ( [key, str(seconds)] if option is None else [key, str(seconds), option.value] ) return cast(bool, await self._execute_command(RequestType.Expire, args)) async def expireat( - self, key: str, unix_seconds: int, option: Optional[ExpireOptions] = None + self, + key: TEncodable, + unix_seconds: int, + option: Optional[ExpireOptions] = None, ) -> bool: """ Sets a timeout on `key` using an absolute Unix timestamp (seconds since January 1, 1970) instead of specifying the @@ -1398,10 +2385,10 @@ async def expireat( deleted. If `key` already has an existing expire set, the time to live is updated to the new value. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/expireat/ for more details. + See https://valkey.io/commands/expireat/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. unix_seconds (int): The timeout in an absolute Unix timestamp. option (Optional[ExpireOptions]): The expire option. @@ -1421,17 +2408,20 @@ async def expireat( return cast(bool, await self._execute_command(RequestType.ExpireAt, args)) async def pexpire( - self, key: str, milliseconds: int, option: Optional[ExpireOptions] = None + self, + key: TEncodable, + milliseconds: int, + option: Optional[ExpireOptions] = None, ) -> bool: """ Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. If `key` already has an existing expire set, the time to live is updated to the new value. If `milliseconds` is a non-positive number, the key will be deleted rather than expired. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/pexpire/ for more details. + See https://valkey.io/commands/pexpire/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. milliseconds (int): The timeout in milliseconds. option (Optional[ExpireOptions]): The expire option. @@ -1451,7 +2441,10 @@ async def pexpire( return cast(bool, await self._execute_command(RequestType.PExpire, args)) async def pexpireat( - self, key: str, unix_milliseconds: int, option: Optional[ExpireOptions] = None + self, + key: TEncodable, + unix_milliseconds: int, + option: Optional[ExpireOptions] = None, ) -> bool: """ Sets a timeout on `key` using an absolute Unix timestamp in milliseconds (milliseconds since January 1, 1970) instead @@ -1460,10 +2453,10 @@ async def pexpireat( deleted. If `key` already has an existing expire set, the time to live is updated to the new value. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/pexpireat/ for more details. + See https://valkey.io/commands/pexpireat/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. unix_milliseconds (int): The timeout in an absolute Unix timestamp in milliseconds. option (Optional[ExpireOptions]): The expire option. @@ -1482,13 +2475,68 @@ async def pexpireat( ) return cast(bool, await self._execute_command(RequestType.PExpireAt, args)) - async def ttl(self, key: str) -> int: + async def expiretime(self, key: TEncodable) -> int: + """ + Returns the absolute Unix timestamp (since January 1, 1970) at which + the given `key` will expire, in seconds. + To get the expiration with millisecond precision, use `pexpiretime`. + + See https://valkey.io/commands/expiretime/ for details. + + Args: + key (TEncodable): The `key` to determine the expiration value of. + + Returns: + int: The expiration Unix timestamp in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. + + Examples: + >>> await client.expiretime("my_key") + -2 # 'my_key' doesn't exist. + >>> await client.set("my_key", "value") + >>> await client.expiretime("my_key") + -1 # 'my_key' has no associate expiration. + >>> await client.expire("my_key", 60) + >>> await client.expiretime("my_key") + 1718614954 + + Since: Valkey version 7.0.0. + """ + return cast(int, await self._execute_command(RequestType.ExpireTime, [key])) + + async def pexpiretime(self, key: TEncodable) -> int: + """ + Returns the absolute Unix timestamp (since January 1, 1970) at which + the given `key` will expire, in milliseconds. + + See https://valkey.io/commands/pexpiretime/ for details. + + Args: + key (TEncodable): The `key` to determine the expiration value of. + + Returns: + int: The expiration Unix timestamp in milliseconds, -2 if `key` does not exist, or -1 if `key` exists but has no associated expiration. + + Examples: + >>> await client.pexpiretime("my_key") + -2 # 'my_key' doesn't exist. + >>> await client.set("my_key", "value") + >>> await client.pexpiretime("my_key") + -1 # 'my_key' has no associate expiration. + >>> await client.expire("my_key", 60) + >>> await client.pexpiretime("my_key") + 1718615446670 + + Since: Valkey version 7.0.0. + """ + return cast(int, await self._execute_command(RequestType.PExpireTime, [key])) + + async def ttl(self, key: TEncodable) -> int: """ Returns the remaining time to live of `key` that has a timeout. - See https://redis.io/commands/ttl/ for more details. + See https://valkey.io/commands/ttl/ for more details. Args: - key (str): The key to return its timeout. + key (TEncodable): The key to return its timeout. Returns: int: TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. @@ -1505,14 +2553,14 @@ async def ttl(self, key: str) -> int: async def pttl( self, - key: str, + key: TEncodable, ) -> int: """ Returns the remaining time to live of `key` that has a timeout, in milliseconds. - See https://redis.io/commands/pttl for more details. + See https://valkey.io/commands/pttl for more details. Args: - key (str): The key to return its timeout. + key (TEncodable): The key to return its timeout. Returns: int: TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. @@ -1530,16 +2578,16 @@ async def pttl( async def persist( self, - key: str, + key: TEncodable, ) -> bool: """ Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to persistent (a key that will never expire as no timeout is associated). - See https://redis.io/commands/persist/ for more details. + See https://valkey.io/commands/persist/ for more details. Args: - key (str): TThe key to remove the existing timeout on. + key (TEncodable): The key to remove the existing timeout on. Returns: bool: False if `key` does not exist or does not have an associated timeout, True if the timeout has been removed. @@ -1553,712 +2601,4030 @@ async def persist( await self._execute_command(RequestType.Persist, [key]), ) - async def type(self, key: str) -> str: + async def type(self, key: TEncodable) -> bytes: """ - Returns the string representation of the type of the value stored at `key`. + Returns the bytes string representation of the type of the value stored at `key`. - See https://redis.io/commands/type/ for more details. + See https://valkey.io/commands/type/ for more details. Args: - key (str): The key to check its data type. + key (TEncodable): The key to check its data type. Returns: - str: If the key exists, the type of the stored value is returned. - Otherwise, a "none" string is returned. + bytes: If the key exists, the type of the stored value is returned. + Otherwise, a b"none" bytes string is returned. Examples: >>> await client.set("key", "value") >>> await client.type("key") - 'string' + b'string' >>> await client.lpush("key", ["value"]) >>> await client.type("key") - 'list' + b'list' """ - return cast(str, await self._execute_command(RequestType.Type, [key])) + return cast(bytes, await self._execute_command(RequestType.Type, [key])) - async def geoadd( + async def xadd( self, - key: str, - members_geospatialdata: Mapping[str, GeospatialData], - existing_options: Optional[ConditionalChange] = None, - changed: bool = False, - ) -> int: + key: TEncodable, + values: List[Tuple[TEncodable, TEncodable]], + options: Optional[StreamAddOptions] = None, + ) -> Optional[bytes]: """ - Adds geospatial members with their positions to the specified sorted set stored at `key`. - If a member is already a part of the sorted set, its position is updated. + Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. - See https://valkey.io/commands/geoadd for more details. + See https://valkey.io/commands/xadd for more details. Args: - key (str): The key of the sorted set. - members_geospatialdata (Mapping[str, GeospatialData]): A mapping of member names to their corresponding positions. See `GeospatialData`. - The command will report an error when the user attempts to index coordinates outside the specified ranges. - existing_options (Optional[ConditionalChange]): Options for handling existing members. - - NX: Only add new elements. - - XX: Only update existing elements. - changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + key (TEncodable): The key of the stream. + values (List[Tuple[TEncodable, TEncodable]]): Field-value pairs to be added to the entry. + options (Optional[StreamAddOptions]): Additional options for adding entries to the stream. Default to None. See `StreamAddOptions`. Returns: - int: The number of elements added to the sorted set. - If `changed` is set, returns the number of elements updated in the sorted set. - - Examples: - >>> await client.geoadd("my_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) - 2 # Indicates that two elements have been added to the sorted set "my_sorted_set". - >>> await client.geoadd("my_sorted_set", {"Palermo": GeospatialData(14.361389, 38.115556)}, existing_options=ConditionalChange.XX, changed=True) - 1 # Updates the position of an existing member in the sorted set "my_sorted_set". - """ - args = [key] - if existing_options: - args.append(existing_options.value) - - if changed: - args.append("CH") + bytes: The id of the added entry, or None if `options.make_stream` is set to False and no stream with the matching `key` exists. - members_geospatialdata_list = [ - coord - for member, position in members_geospatialdata.items() - for coord in [str(position.longitude), str(position.latitude), member] - ] - args += members_geospatialdata_list + Example: + >>> await client.xadd("mystream", [("field", "value"), ("field2", "value2")]) + b"1615957011958-0" # Example stream entry ID. + >>> await client.xadd("non_existing_stream", [(field, "foo1"), (field2, "bar1")], StreamAddOptions(id="0-1", make_stream=False)) + None # The key doesn't exist, therefore, None is returned. + >>> await client.xadd("non_existing_stream", [(field, "foo1"), (field2, "bar1")], StreamAddOptions(id="0-1")) + b"0-1" # Returns the stream id. + """ + args: List[TEncodable] = [key] + if options: + args.extend(options.to_args()) + else: + args.append("*") + args.extend([field for pair in values for field in pair]) return cast( - int, - await self._execute_command(RequestType.GeoAdd, args), + Optional[bytes], await self._execute_command(RequestType.XAdd, args) ) - async def geohash(self, key: str, members: List[str]) -> List[Optional[str]]: + async def xdel(self, key: TEncodable, ids: List[TEncodable]) -> int: """ - Returns the GeoHash strings representing the positions of all the specified members in the sorted set stored at - `key`. + Removes the specified entries by id from a stream, and returns the number of entries deleted. - See https://valkey.io/commands/geohash for more details. + See https://valkey.io/commands/xdel for more details. Args: - key (str): The key of the sorted set. - members (List[str]): The list of members whose GeoHash strings are to be retrieved. + key (TEncodable): The key of the stream. + ids (List[TEncodable]): An array of entry ids. Returns: - List[Optional[str]]: A list of GeoHash strings representing the positions of the specified members stored at `key`. - If a member does not exist in the sorted set, a None value is returned for that member. + int: The number of entries removed from the stream. This number may be less than the number of entries in + `ids`, if the specified `ids` don't exist in the stream. Examples: - >>> await client.geoadd("my_geo_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) - >>> await client.geohash("my_geo_sorted_set", ["Palermo", "Catania", "some city]) - ["sqc8b49rny0", "sqdtr74hyu0", None] # Indicates the GeoHash strings for the specified members. + >>> await client.xdel("key", ["1538561698944-0", "1538561698944-1"]) + 2 # Stream marked 2 entries as deleted. """ + args: List[TEncodable] = [key] + args.extend(ids) return cast( - List[Optional[str]], - await self._execute_command(RequestType.GeoHash, [key] + members), + int, + await self._execute_command(RequestType.XDel, [key] + ids), ) - async def zadd( + async def xtrim( self, - key: str, - members_scores: Mapping[str, float], - existing_options: Optional[ConditionalChange] = None, - update_condition: Optional[UpdateOptions] = None, - changed: bool = False, + key: TEncodable, + options: StreamTrimOptions, ) -> int: """ - Adds members with their scores to the sorted set stored at `key`. - If a member is already a part of the sorted set, its score is updated. + Trims the stream stored at `key` by evicting older entries. - See https://redis.io/commands/zadd/ for more details. + See https://valkey.io/commands/xtrim for more details. Args: - key (str): The key of the sorted set. - members_scores (Mapping[str, float]): A mapping of members to their corresponding scores. - existing_options (Optional[ConditionalChange]): Options for handling existing members. - - NX: Only add new elements. - - XX: Only update existing elements. - update_condition (Optional[UpdateOptions]): Options for updating scores. - - GT: Only update scores greater than the current values. - - LT: Only update scores less than the current values. - changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + key (TEncodable): The key of the stream. + options (StreamTrimOptions): Options detailing how to trim the stream. See `StreamTrimOptions`. Returns: - int: The number of elements added to the sorted set. - If `changed` is set, returns the number of elements updated in the sorted set. + int: TThe number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. - Examples: - >>> await client.zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) - 2 # Indicates that two elements have been added to the sorted set "my_sorted_set." - >>> await client.zadd("existing_sorted_set", {"member1": 15.0, "member2": 5.5}, existing_options=ConditionalChange.XX, changed=True) - 2 # Updates the scores of two existing members in the sorted set "existing_sorted_set." + Example: + >>> await client.xadd("mystream", [("field", "value"), ("field2", "value2")], StreamAddOptions(id="0-1")) + >>> await client.xtrim("mystream", TrimByMinId(exact=True, threshold="0-2"))) + 1 # One entry was deleted from the stream. """ args = [key] - if existing_options: - args.append(existing_options.value) + if options: + args.extend(options.to_args()) - if update_condition: - args.append(update_condition.value) + return cast(int, await self._execute_command(RequestType.XTrim, args)) - if changed: - args.append("CH") + async def xlen(self, key: TEncodable) -> int: + """ + Returns the number of entries in the stream stored at `key`. - if existing_options and update_condition: - if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: - raise ValueError( - "The GT, LT and NX options are mutually exclusive. " - f"Cannot choose both {update_condition.value} and NX." - ) + See https://valkey.io/commands/xlen for more details. - members_scores_list = [ - str(item) for pair in members_scores.items() for item in pair[::-1] - ] - args += members_scores_list + Args: + key (TEncodable): The key of the stream. + + Returns: + int: The number of entries in the stream. If `key` does not exist, returns 0. + Examples: + >>> await client.xadd("mystream", [("field", "value")]) + >>> await client.xadd("mystream", [("field2", "value2")]) + >>> await client.xlen("mystream") + 2 # There are 2 entries in "mystream". + """ return cast( int, - await self._execute_command(RequestType.Zadd, args), + await self._execute_command(RequestType.XLen, [key]), ) - async def zadd_incr( + async def xrange( self, - key: str, - member: str, - increment: float, - existing_options: Optional[ConditionalChange] = None, - update_condition: Optional[UpdateOptions] = None, - ) -> Optional[float]: + key: TEncodable, + start: StreamRangeBound, + end: StreamRangeBound, + count: Optional[int] = None, + ) -> Optional[Mapping[bytes, List[List[bytes]]]]: """ - Increments the score of member in the sorted set stored at `key` by `increment`. - If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). - If `key` does not exist, a new sorted set with the specified member as its sole member is created. + Returns stream entries matching a given range of IDs. - See https://redis.io/commands/zadd/ for more details. + See https://valkey.io/commands/xrange for more details. Args: - key (str): The key of the sorted set. - member (str): A member in the sorted set to increment. - increment (float): The score to increment the member. - existing_options (Optional[ConditionalChange]): Options for handling the member's existence. - - NX: Only increment a member that doesn't exist. - - XX: Only increment an existing member. - update_condition (Optional[UpdateOptions]): Options for updating the score. - - GT: Only increment the score of the member if the new score will be greater than the current score. - - LT: Only increment (decrement) the score of the member if the new score will be less than the current score. + key (TEncodable): The key of the stream. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. Returns: - Optional[float]: The score of the member. - If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. + Optional[Mapping[bytes, List[List[bytes]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range + arguments are not applicable. Examples: - >>> await client.zadd_incr("my_sorted_set", member , 5.0) - 5.0 - >>> await client.zadd_incr("existing_sorted_set", member , "3.0" , UpdateOptions.LESS_THAN) - None + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xrange("mystream", MinId(), MaxId()) + { + b"0-1": [[b"field1", b"value1"]], + b"0-2": [[b"field2", b"value2"], [b"field2", b"value3"]], + } # Indicates the stream IDs and their associated field-value pairs for all stream entries in "mystream". + """ + args: List[TEncodable] = [key, start.to_arg(), end.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) + + return cast( + Optional[Mapping[bytes, List[List[bytes]]]], + await self._execute_command(RequestType.XRange, args), + ) + + async def xrevrange( + self, + key: TEncodable, + end: StreamRangeBound, + start: StreamRangeBound, + count: Optional[int] = None, + ) -> Optional[Mapping[bytes, List[List[bytes]]]]: """ - args = [key] - if existing_options: - args.append(existing_options.value) + Returns stream entries matching a given range of IDs in reverse order. Equivalent to `XRANGE` but returns the + entries in reverse order. - if update_condition: - args.append(update_condition.value) + See https://valkey.io/commands/xrevrange for more details. - args.append("INCR") + Args: + key (TEncodable): The key of the stream. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. - if existing_options and update_condition: - if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: - raise ValueError( - "The GT, LT and NX options are mutually exclusive. " - f"Cannot choose both {update_condition.value} and NX." - ) + Returns: + Optional[Mapping[bytes, List[List[bytes]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range + arguments are not applicable. + + Examples: + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xrevrange("mystream", MaxId(), MinId()) + { + "0-2": [["field2", "value2"], ["field2", "value3"]], + "0-1": [["field1", "value1"]], + } # Indicates the stream IDs and their associated field-value pairs for all stream entries in "mystream". + """ + args: List[TEncodable] = [key, end.to_arg(), start.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) - args += [str(increment), member] return cast( - Optional[float], - await self._execute_command(RequestType.Zadd, args), + Optional[Mapping[bytes, List[List[bytes]]]], + await self._execute_command(RequestType.XRevRange, args), ) - async def zcard(self, key: str) -> int: + async def xread( + self, + keys_and_ids: Mapping[TEncodable, TEncodable], + options: Optional[StreamReadOptions] = None, + ) -> Optional[Mapping[bytes, Mapping[bytes, List[List[bytes]]]]]: """ - Returns the cardinality (number of elements) of the sorted set stored at `key`. + Reads entries from the given streams. + + See https://valkey.io/commands/xread for more details. - See https://redis.io/commands/zcard/ for more details. + Note: + When in cluster mode, all keys in `keys_and_ids` must map to the same hash slot. Args: - key (str): The key of the sorted set. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of keys and entry IDs to read from. The mapping is composed of a + stream's key and the ID of the entry after which the stream will be read. + options (Optional[StreamReadOptions]): Options detailing how to read the stream. Returns: - int: The number of elements in the sorted set. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + Optional[Mapping[bytes, Mapping[bytes, List[List[bytes]]]]]: A mapping of stream keys, to a mapping of stream IDs, + to a list of pairings with format `[[field, entry], [field, entry], ...]`. + None will be returned under the following conditions: + - All key-ID pairs in `keys_and_ids` have either a non-existing key or a non-existing ID, or there are no entries after the given ID. + - The `BLOCK` option is specified and the timeout is hit. Examples: - >>> await client.zcard("my_sorted_set") - 3 # Indicates that there are 3 elements in the sorted set "my_sorted_set". - >>> await client.zcard("non_existing_key") - 0 - """ - return cast(int, await self._execute_command(RequestType.Zcard, [key])) + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xread({"mystream": "0-0"}, StreamReadOptions(block_ms=1000)) + { + b"mystream": { + b"0-1": [[b"field1", b"value1"]], + b"0-2": [[b"field2", b"value2"], [b"field2", b"value3"]], + } + } + # Indicates the stream entries for "my_stream" with IDs greater than "0-0". The operation blocks up to + # 1000ms if there is no stream data. + """ + args: List[TEncodable] = [] if options is None else options.to_args() + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) - async def zcount( + return cast( + Optional[Mapping[bytes, Mapping[bytes, List[List[bytes]]]]], + await self._execute_command(RequestType.XRead, args), + ) + + async def xgroup_create( self, - key: str, - min_score: Union[InfBound, ScoreBoundary], - max_score: Union[InfBound, ScoreBoundary], - ) -> int: + key: TEncodable, + group_name: TEncodable, + group_id: TEncodable, + options: Optional[StreamGroupOptions] = None, + ) -> TOK: """ - Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. + Creates a new consumer group uniquely identified by `group_name` for the stream stored at `key`. - See https://redis.io/commands/zcount/ for more details. + See https://valkey.io/commands/xgroup-create for more details. Args: - key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. + key (TEncodable): The key of the stream. + group_name (TEncodable): The newly created consumer group name. + group_id (TEncodable): The stream entry ID that specifies the last delivered entry in the stream from the new + group’s perspective. The special ID "$" can be used to specify the last entry in the stream. + options (Optional[StreamGroupOptions]): Options for creating the stream group. Returns: - int: The number of members in the specified score range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `max_score` < `min_score`, 0 is returned. + TOK: A simple "OK" response. Examples: - >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , InfBound.POS_INF) - 2 # Indicates that there are 2 members with scores between 5.0 (not exclusive) and +inf in the sorted set "my_sorted_set". - >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , ScoreBoundary(10.0 , is_inclusive=false)) - 1 # Indicates that there is one ScoreBoundary with 5.0 < score <= 10.0 in the sorted set "my_sorted_set". + >>> await client.xgroup_create("mystream", "mygroup", "$", StreamGroupOptions(make_stream=True)) + OK + # Created the consumer group "mygroup" for the stream "mystream", which will track entries created after + # the most recent entry. The stream was created with length 0 if it did not already exist. """ - score_min = ( - min_score.value["score_arg"] - if type(min_score) == InfBound - else min_score.value - ) - score_max = ( - max_score.value["score_arg"] - if type(max_score) == InfBound - else max_score.value - ) + args: List[TEncodable] = [key, group_name, group_id] + if options is not None: + args.extend(options.to_args()) + return cast( - int, - await self._execute_command( - RequestType.Zcount, [key, score_min, score_max] - ), + TOK, + await self._execute_command(RequestType.XGroupCreate, args), ) - async def zpopmax( - self, key: str, count: Optional[int] = None - ) -> Mapping[str, float]: + async def xgroup_destroy(self, key: TEncodable, group_name: TEncodable) -> bool: """ - Removes and returns the members with the highest scores from the sorted set stored at `key`. - If `count` is provided, up to `count` members with the highest scores are removed and returned. - Otherwise, only one member with the highest score is removed and returned. + Destroys the consumer group `group_name` for the stream stored at `key`. - See https://redis.io/commands/zpopmax for more details. + See https://valkey.io/commands/xgroup-destroy for more details. Args: - key (str): The key of the sorted set. - count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. - If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name to delete. Returns: - Mapping[str, float]: A map of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. - If `key` doesn't exist, it will be treated as an empy sorted set and the command returns an empty map. + bool: True if the consumer group was destroyed. Otherwise, returns False. Examples: - >>> await client.zpopmax("my_sorted_set") - {'member1': 10.0} # Indicates that 'member1' with a score of 10.0 has been removed from the sorted set. - >>> await client.zpopmax("my_sorted_set", 2) - {'member2': 8.0, 'member3': 7.5} # Indicates that 'member2' with a score of 8.0 and 'member3' with a score of 7.5 have been removed from the sorted set. + >>> await client.xgroup_destroy("mystream", "mygroup") + True # The consumer group "mygroup" for stream "mystream" was destroyed. """ return cast( - Mapping[str, float], - await self._execute_command( - RequestType.ZPopMax, [key, str(count)] if count else [key] - ), + bool, + await self._execute_command(RequestType.XGroupDestroy, [key, group_name]), ) - async def zpopmin( - self, key: str, count: Optional[int] = None - ) -> Mapping[str, float]: + async def xgroup_create_consumer( + self, + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + ) -> bool: """ - Removes and returns the members with the lowest scores from the sorted set stored at `key`. - If `count` is provided, up to `count` members with the lowest scores are removed and returned. - Otherwise, only one member with the lowest score is removed and returned. + Creates a consumer named `consumer_name` in the consumer group `group_name` for the stream stored at `key`. - See https://redis.io/commands/zpopmin for more details. + See https://valkey.io/commands/xgroup-createconsumer for more details. Args: - key (str): The key of the sorted set. - count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. - If `count` is higher than the sorted set's cardinality, returns all members and their scores. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The newly created consumer. Returns: - Mapping[str, float]: A map of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. - If `key` doesn't exist, it will be treated as an empy sorted set and the command returns an empty map. + bool: True if the consumer is created. Otherwise, returns False. Examples: - >>> await client.zpopmin("my_sorted_set") - {'member1': 5.0} # Indicates that 'member1' with a score of 5.0 has been removed from the sorted set. - >>> await client.zpopmin("my_sorted_set", 2) - {'member3': 7.5 , 'member2': 8.0} # Indicates that 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set. + >>> await client.xgroup_create_consumer("mystream", "mygroup", "myconsumer") + True # The consumer "myconsumer" was created in consumer group "mygroup" for the stream "mystream". """ return cast( - Mapping[str, float], + bool, await self._execute_command( - RequestType.ZPopMin, [key, str(count)] if count else [key] + RequestType.XGroupCreateConsumer, [key, group_name, consumer_name] ), ) - async def zrange( + async def xgroup_del_consumer( self, - key: str, - range_query: Union[RangeByIndex, RangeByLex, RangeByScore], - reverse: bool = False, - ) -> List[str]: + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + ) -> int: """ - Returns the specified range of elements in the sorted set stored at `key`. - - ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + Deletes a consumer named `consumer_name` in the consumer group `group_name` for the stream stored at `key`. - See https://redis.io/commands/zrange/ for more details. - - To get the elements with their scores, see zrange_withscores. + See https://valkey.io/commands/xgroup-delconsumer for more details. Args: - key (str): The key of the sorted set. - range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. - - For range queries by index (rank), use RangeByIndex. - - For range queries by lexicographical order, use RangeByLex. - - For range queries by score, use RangeByScore. - reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer to delete. Returns: - List[str]: A list of elements within the specified range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + int: The number of pending messages the `consumer` had before it was deleted. Examples: - >>> await client.zrange("my_sorted_set", RangeByIndex(0, -1)) - ['member1', 'member2', 'member3'] # Returns all members in ascending order. - >>> await client.zrange("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) - ['member2', 'member3'] # Returns members with scores within the range of negative infinity to 3, in ascending order. + >>> await client.xgroup_del_consumer("mystream", "mygroup", "myconsumer") + 5 # Consumer "myconsumer" was deleted, and had 5 pending messages unclaimed. """ - args = _create_zrange_args(key, range_query, reverse, with_scores=False) - - return cast(List[str], await self._execute_command(RequestType.Zrange, args)) + return cast( + int, + await self._execute_command( + RequestType.XGroupDelConsumer, [key, group_name, consumer_name] + ), + ) - async def zrange_withscores( + async def xgroup_set_id( self, - key: str, - range_query: Union[RangeByIndex, RangeByScore], - reverse: bool = False, - ) -> Mapping[str, float]: + key: TEncodable, + group_name: TEncodable, + stream_id: TEncodable, + entries_read: Optional[int] = None, + ) -> TOK: """ - Returns the specified range of elements with their scores in the sorted set stored at `key`. - Similar to ZRANGE but with a WITHSCORE flag. + Set the last delivered ID for a consumer group. - See https://redis.io/commands/zrange/ for more details. + See https://valkey.io/commands/xgroup-setid for more details. Args: - key (str): The key of the sorted set. - range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. - - For range queries by index (rank), use RangeByIndex. - - For range queries by score, use RangeByScore. - reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + stream_id (TEncodable): The stream entry ID that should be set as the last delivered ID for the consumer group. + entries_read: (Optional[int]): A value representing the number of stream entries already read by the + group. This option can only be specified if you are using Valkey version 7.0.0 or above. Returns: - Mapping[str , float]: A map of elements and their scores within the specified range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + TOK: A simple "OK" response. Examples: - >>> await client.zrange_withscores("my_sorted_set", RangeByScore(ScoreBoundary(10), ScoreBoundary(20))) - {'member1': 10.5, 'member2': 15.2} # Returns members with scores between 10 and 20 with their scores. - >>> await client.zrange_withscores("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) - {'member4': -2.0, 'member7': 1.5} # Returns members with with scores within the range of negative infinity to 3, with their scores. + >>> await client.xgroup_set_id("mystream", "mygroup", "0") + OK # The last delivered ID for consumer group "mygroup" was set to 0. """ - args = _create_zrange_args(key, range_query, reverse, with_scores=True) + args: List[TEncodable] = [key, group_name, stream_id] + if entries_read is not None: + args.extend(["ENTRIESREAD", str(entries_read)]) return cast( - Mapping[str, float], await self._execute_command(RequestType.Zrange, args) + TOK, + await self._execute_command(RequestType.XGroupSetId, args), ) - async def zrank( + async def xreadgroup( self, - key: str, - member: str, - ) -> Optional[int]: + keys_and_ids: Mapping[TEncodable, TEncodable], + group_name: TEncodable, + consumer_name: TEncodable, + options: Optional[StreamReadGroupOptions] = None, + ) -> Optional[Mapping[bytes, Mapping[bytes, Optional[List[List[bytes]]]]]]: """ - Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. + Reads entries from the given streams owned by a consumer group. - See https://redis.io/commands/zrank for more details. + See https://valkey.io/commands/xreadgroup for more details. - To get the rank of `member` with its score, see `zrank_withscore`. + Note: + When in cluster mode, all keys in `keys_and_ids` must map to the same hash slot. Args: - key (str): The key of the sorted set. - member (str): The member whose rank is to be retrieved. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. The special ">" + ID returns messages that were never delivered to any other consumer. Any other valid ID will return + entries pending for the consumer with IDs greater than the one provided. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer name. The consumer will be auto-created if it does not already exist. + options (Optional[StreamReadGroupOptions]): Options detailing how to read the stream. Returns: - Optional[int]: The rank of `member` in the sorted set. - If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + Optional[Mapping[bytes, Mapping[bytes, Optional[List[List[bytes]]]]]]: A mapping of stream keys, to a mapping of + stream IDs, to a list of pairings with format `[[field, entry], [field, entry], ...]`. + Returns None if the BLOCK option is given and a timeout occurs, or if there is no stream that can be served. + + Examples: + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="1-0")) + >>> await client.xgroup_create("mystream", "mygroup", "0-0") + >>> await client.xreadgroup({"mystream": ">"}, "mygroup", "myconsumer", StreamReadGroupOptions(count=1)) + { + b"mystream": { + b"1-0": [[b"field1", b"value1"]], + } + } # Read one stream entry from "mystream" using "myconsumer" in the consumer group "mygroup". + """ + args: List[TEncodable] = ["GROUP", group_name, consumer_name] + if options is not None: + args.extend(options.to_args()) + + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) - Examples: - >>> await client.zrank("my_sorted_set", "member2") - 1 # Indicates that "member2" has the second-lowest score in the sorted set "my_sorted_set". - >>> await client.zrank("my_sorted_set", "non_existing_member") - None # Indicates that "non_existing_member" is not present in the sorted set "my_sorted_set". - """ return cast( - Optional[int], await self._execute_command(RequestType.Zrank, [key, member]) + Optional[Mapping[bytes, Mapping[bytes, Optional[List[List[bytes]]]]]], + await self._execute_command(RequestType.XReadGroup, args), ) - async def zrank_withscore( + async def xack( self, - key: str, - member: str, - ) -> Optional[List[Union[int, float]]]: + key: TEncodable, + group_name: TEncodable, + ids: List[TEncodable], + ) -> int: """ - Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. + Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group. + This command should be called on pending messages so that such messages do not get processed again by the + consumer group. - See https://redis.io/commands/zrank for more details. + See https://valkey.io/commands/xack for more details. Args: - key (str): The key of the sorted set. - member (str): The member whose rank is to be retrieved. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + ids (List[TEncodable]): The stream entry IDs to acknowledge and consume for the given consumer group. Returns: - Optional[List[Union[int, float]]]: A list containing the rank and score of `member` in the sorted set. - If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + int: The number of messages that were successfully acknowledged. Examples: - >>> await client.zrank_withscore("my_sorted_set", "member2") - [1 , 6.0] # Indicates that "member2" with score 6.0 has the second-lowest score in the sorted set "my_sorted_set". - >>> await client.zrank_withscore("my_sorted_set", "non_existing_member") - None # Indicates that "non_existing_member" is not present in the sorted set "my_sorted_set". + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="1-0")) + >>> await client.xgroup_create("mystream", "mygroup", "0-0") + >>> await client.xreadgroup({"mystream": ">"}, "mygroup", "myconsumer") + { + "mystream": { + "1-0": [["field1", "value1"]], + } + } # Read one stream entry, the entry is now in the Pending Entries List for "mygroup". + >>> await client.xack("mystream", "mygroup", ["1-0"]) + 1 # 1 pending message was acknowledged and removed from the Pending Entries List for "mygroup". + """ + args: List[TEncodable] = [key, group_name] + args.extend(ids) + return cast( + int, + await self._execute_command(RequestType.XAck, [key, group_name] + ids), + ) + + async def xpending( + self, + key: TEncodable, + group_name: TEncodable, + ) -> List[Union[int, bytes, List[List[bytes]], None]]: + """ + Returns stream message summary information for pending messages for the given consumer group. + + See https://valkey.io/commands/xpending for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + + Returns: + List[Union[int, bytes, List[List[bytes]], None]]: A list that includes the summary of pending messages, with the + format `[num_group_messages, start_id, end_id, [[consumer_name, num_consumer_messages]]]`, where: + - `num_group_messages`: The total number of pending messages for this consumer group. + - `start_id`: The smallest ID among the pending messages. + - `end_id`: The greatest ID among the pending messages. + - `[[consumer_name, num_consumer_messages]]`: A 2D list of every consumer in the consumer group with at + least one pending message, and the number of pending messages it has. + + If there are no pending messages for the given consumer group, `[0, None, None, None]` will be returned. - Since: Redis version 7.2.0. + Examples: + >>> await client.xpending("my_stream", "my_group") + [4, "1-0", "1-3", [["my_consumer1", "3"], ["my_consumer2", "1"]] """ return cast( - Optional[List[Union[int, float]]], - await self._execute_command(RequestType.Zrank, [key, member, "WITHSCORE"]), + List[Union[int, bytes, List[List[bytes]], None]], + await self._execute_command(RequestType.XPending, [key, group_name]), ) - async def zrem( + async def xpending_range( self, - key: str, - members: List[str], - ) -> int: + key: TEncodable, + group_name: TEncodable, + start: StreamRangeBound, + end: StreamRangeBound, + count: int, + options: Optional[StreamPendingOptions] = None, + ) -> List[List[Union[bytes, int]]]: """ - Removes the specified members from the sorted set stored at `key`. - Specified members that are not a member of this set are ignored. + Returns an extended form of stream message information for pending messages matching a given range of IDs. - See https://redis.io/commands/zrem/ for more details. + See https://valkey.io/commands/xpending for more details. Args: - key (str): The key of the sorted set. - members (List[str]): A list of members to remove from the sorted set. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + count (int): Limits the number of messages returned. + options (Optional[StreamPendingOptions]): The stream pending options. Returns: - int: The number of members that were removed from the sorted set, not including non-existing members. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + List[List[Union[bytes, int]]]: A list of lists, where each inner list is a length 4 list containing extended + message information with the format `[[id, consumer_name, time_elapsed, num_delivered]]`, where: + - `id`: The ID of the message. + - `consumer_name`: The name of the consumer that fetched the message and has still to acknowledge it. We + call it the current owner of the message. + - `time_elapsed`: The number of milliseconds that elapsed since the last time this message was delivered + to this consumer. + - `num_delivered`: The number of times this message was delivered. Examples: - >>> await client.zrem("my_sorted_set", ["member1", "member2"]) - 2 # Indicates that two members have been removed from the sorted set "my_sorted_set." - >>> await client.zrem("non_existing_sorted_set", ["member1", "member2"]) - 0 # Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. + >>> await client.xpending_range("my_stream", "my_group", MinId(), MaxId(), 10, StreamPendingOptions(consumer_name="my_consumer")) + [[b"1-0", b"my_consumer", 1234, 1], [b"1-1", b"my_consumer", 1123, 1]] + # Extended stream entry information for the pending entries associated with "my_consumer". """ + args = _create_xpending_range_args(key, group_name, start, end, count, options) return cast( - int, - await self._execute_command(RequestType.Zrem, [key] + members), + List[List[Union[bytes, int]]], + await self._execute_command(RequestType.XPending, args), ) - async def zremrangebyscore( + async def xclaim( self, - key: str, - min_score: Union[InfBound, ScoreBoundary], - max_score: Union[InfBound, ScoreBoundary], - ) -> int: + key: TEncodable, + group: TEncodable, + consumer: TEncodable, + min_idle_time_ms: int, + ids: List[TEncodable], + options: Optional[StreamClaimOptions] = None, + ) -> Mapping[bytes, List[List[bytes]]]: """ - Removes all elements in the sorted set stored at `key` with a score between `min_score` and `max_score`. + Changes the ownership of a pending message. - See https://redis.io/commands/zremrangebyscore/ for more details. + See https://valkey.io/commands/xclaim for more details. Args: - key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreBoundary]): The minimum score to remove from. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreBoundary]): The maximum score to remove up to. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. + key (TEncodable): The key of the stream. + group (TEncodable): The consumer group name. + consumer (TEncodable): The group consumer. + min_idle_time_ms (int): The minimum idle time for the message to be claimed. + ids (List[TEncodable]): A array of entry ids. + options (Optional[StreamClaimOptions]): Stream claim options. + Returns: - int: The number of members that were removed from the sorted set. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `min_score` is greater than `max_score`, 0 is returned. + A Mapping of message entries with the format + {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. Examples: - >>> await client.zremrangebyscore("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , InfBound.POS_INF) - 2 # Indicates that 2 members with scores between 5.0 (not exclusive) and +inf have been removed from the sorted set "my_sorted_set". - >>> await client.zremrangebyscore("non_existing_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , ScoreBoundary(10.0 , is_inclusive=false)) - 0 # Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. + # read messages from streamId for consumer1 + >>> await client.xreadgroup({"mystream": ">"}, "mygroup", "consumer1") + { + b"mystream": { + b"1-0": [[b"field1", b"value1"]], + } + } + # "1-0" is now read, and we can assign the pending messages to consumer2 + >>> await client.xclaim("mystream", "mygroup", "consumer2", 0, ["1-0"]) + {b"1-0": [[b"field1", b"value1"]]} """ - score_min = ( - min_score.value["score_arg"] - if type(min_score) == InfBound - else min_score.value - ) - score_max = ( - max_score.value["score_arg"] - if type(max_score) == InfBound - else max_score.value - ) + + args = [key, group, consumer, str(min_idle_time_ms), *ids] + + if options: + args.extend(options.to_args()) return cast( - int, - await self._execute_command( - RequestType.ZRemRangeByScore, [key, score_min, score_max] - ), + Mapping[bytes, List[List[bytes]]], + await self._execute_command(RequestType.XClaim, args), ) - async def zremrangebylex( + async def xclaim_just_id( self, - key: str, - min_lex: Union[InfBound, LexBoundary], - max_lex: Union[InfBound, LexBoundary], - ) -> int: + key: TEncodable, + group: TEncodable, + consumer: TEncodable, + min_idle_time_ms: int, + ids: List[TEncodable], + options: Optional[StreamClaimOptions] = None, + ) -> List[TEncodable]: """ - Removes all elements in the sorted set stored at `key` with a lexicographical order between `min_lex` and - `max_lex`. + Changes the ownership of a pending message. This function returns a List with + only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. - See https://redis.io/commands/zremrangebylex/ for more details. + See https://valkey.io/commands/xclaim for more details. Args: - key (str): The key of the sorted set. - min_lex (Union[InfBound, LexBoundary]): The minimum bound of the lexicographical range. - Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` - representing a specific lex and inclusivity. - max_lex (Union[InfBound, LexBoundary]): The maximum bound of the lexicographical range. - Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` - representing a specific lex and inclusivity. + key (TEncodable): The key of the stream. + group (TEncodable): The consumer group name. + consumer (TEncodable): The group consumer. + min_idle_time_ms (int): The minimum idle time for the message to be claimed. + ids (List[TEncodable]): A array of entry ids. + options (Optional[StreamClaimOptions]): Stream claim options. Returns: - int: The number of members that were removed from the sorted set. - If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. - If `min_lex` is greater than `max_lex`, `0` is returned. + A List of message ids claimed by the consumer. Examples: - >>> await client.zremrangebylex("my_sorted_set", LexBoundary("a", is_inclusive=False), LexBoundary("e")) - 4 # Indicates that 4 members, with lexicographical values ranging from "a" (exclusive) to "e" (inclusive), have been removed from "my_sorted_set". - >>> await client.zremrangebylex("non_existing_sorted_set", InfBound.NEG_INF, LexBoundary("e")) - 0 # Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. - """ - min_lex_arg = ( - min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value + # read messages from streamId for consumer1 + >>> await client.xreadgroup({"mystream": ">"}, "mygroup", "consumer1") + { + b"mystream": { + b"1-0": [[b"field1", b"value1"]], + } + } + # "1-0" is now read, and we can assign the pending messages to consumer2 + >>> await client.xclaim_just_id("mystream", "mygroup", "consumer2", 0, ["1-0"]) + ["1-0"] + """ + + args = [ + key, + group, + consumer, + str(min_idle_time_ms), + *ids, + StreamClaimOptions.JUST_ID_VALKEY_API, + ] + + if options: + args.extend(options.to_args()) + + return cast( + List[TEncodable], + await self._execute_command(RequestType.XClaim, args), ) - max_lex_arg = ( - max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value + + async def xautoclaim( + self, + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + min_idle_time_ms: int, + start: TEncodable, + count: Optional[int] = None, + ) -> List[Union[bytes, Mapping[bytes, List[List[bytes]]], List[bytes]]]: + """ + Transfers ownership of pending stream entries that match the specified criteria. + + See https://valkey.io/commands/xautoclaim for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer name. + min_idle_time_ms (int): Filters the claimed entries to those that have been idle for more than the specified + value. + start (TEncodable): Filters the claimed entries to those that have an ID equal or greater than the specified value. + count (Optional[int]): Limits the number of claimed entries to the specified value. + + Returns: + List[Union[bytes, Mapping[bytes, List[List[bytes]]], List[bytes]]]: A list containing the following elements: + - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is equivalent + to the next ID in the stream after the entries that were scanned, or "0-0" if the entire stream was + scanned. + - A mapping of the claimed entries, with the keys being the claimed entry IDs and the values being a + 2D list of the field-value pairs in the format `[[field1, value1], [field2, value2], ...]`. + - If you are using Valkey 7.0.0 or above, the response list will also include a list containing the + message IDs that were in the Pending Entries List but no longer exist in the stream. These IDs are + deleted from the Pending Entries List. + + Examples: + # Valkey version < 7.0.0: + >>> await client.xautoclaim("my_stream", "my_group", "my_consumer", 3_600_000, "0-0") + [ + b"0-0", + { + b"1-1": [ + [b"field1", b"value1"], + [b"field2", b"value2"], + ] + } + ] + # Stream entry "1-1" was idle for over an hour and was thus claimed by "my_consumer". The entire stream + # was scanned. + + # Valkey version 7.0.0 and above: + >>> await client.xautoclaim("my_stream", "my_group", "my_consumer", 3_600_000, "0-0") + [ + b"0-0", + { + b"1-1": [ + [b"field1", b"value1"], + [b"field2", b"value2"], + ] + }, + [b"1-2"] + ] + # Stream entry "1-1" was idle for over an hour and was thus claimed by "my_consumer". The entire stream + # was scanned. Additionally, entry "1-2" was removed from the Pending Entries List because it no longer + # exists in the stream. + + Since: Valkey version 6.2.0. + """ + args: List[TEncodable] = [ + key, + group_name, + consumer_name, + str(min_idle_time_ms), + start, + ] + if count is not None: + args.extend(["COUNT", str(count)]) + + return cast( + List[Union[bytes, Mapping[bytes, List[List[bytes]]], List[bytes]]], + await self._execute_command(RequestType.XAutoClaim, args), ) + async def xautoclaim_just_id( + self, + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + min_idle_time_ms: int, + start: TEncodable, + count: Optional[int] = None, + ) -> List[Union[bytes, List[bytes]]]: + """ + Transfers ownership of pending stream entries that match the specified criteria. This command uses the JUSTID + argument to further specify that the return value should contain a list of claimed IDs without their + field-value info. + + See https://valkey.io/commands/xautoclaim for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer name. + min_idle_time_ms (int): Filters the claimed entries to those that have been idle for more than the specified + value. + start (TEncodable): Filters the claimed entries to those that have an ID equal or greater than the specified value. + count (Optional[int]): Limits the number of claimed entries to the specified value. + + Returns: + List[Union[bytes, List[bytes]]]: A list containing the following elements: + - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is equivalent + to the next ID in the stream after the entries that were scanned, or "0-0" if the entire stream was + scanned. + - A list of the IDs for the claimed entries. + - If you are using Valkey 7.0.0 or above, the response list will also include a list containing the + message IDs that were in the Pending Entries List but no longer exist in the stream. These IDs are + deleted from the Pending Entries List. + + Examples: + # Valkey version < 7.0.0: + >>> await client.xautoclaim_just_id("my_stream", "my_group", "my_consumer", 3_600_000, "0-0") + [b"0-0", [b"1-1"]] + # Stream entry "1-1" was idle for over an hour and was thus claimed by "my_consumer". The entire stream + # was scanned. + + # Valkey version 7.0.0 and above: + >>> await client.xautoclaim_just_id("my_stream", "my_group", "my_consumer", 3_600_000, "0-0") + [b"0-0", [b"1-1"], [b"1-2"]] + # Stream entry "1-1" was idle for over an hour and was thus claimed by "my_consumer". The entire stream + # was scanned. Additionally, entry "1-2" was removed from the Pending Entries List because it no longer + # exists in the stream. + + Since: Valkey version 6.2.0. + """ + args: List[TEncodable] = [ + key, + group_name, + consumer_name, + str(min_idle_time_ms), + start, + ] + if count is not None: + args.extend(["COUNT", str(count)]) + + args.append("JUSTID") + return cast( - int, - await self._execute_command( - RequestType.ZRemRangeByLex, [key, min_lex_arg, max_lex_arg] - ), + List[Union[bytes, List[bytes]]], + await self._execute_command(RequestType.XAutoClaim, args), ) - async def zlexcount( + async def xinfo_groups( self, - key: str, - min_lex: Union[InfBound, LexBoundary], - max_lex: Union[InfBound, LexBoundary], - ) -> int: + key: TEncodable, + ) -> List[Mapping[bytes, Union[bytes, int, None]]]: """ - Returns the number of members in the sorted set stored at `key` with lexicographical values between `min_lex` and `max_lex`. + Returns the list of all consumer groups and their attributes for the stream stored at `key`. - See https://redis.io/commands/zlexcount/ for more details. + See https://valkey.io/commands/xinfo-groups for more details. Args: - key (str): The key of the sorted set. - min_lex (Union[InfBound, LexBoundary]): The minimum lexicographical value to count from. - Can be an instance of InfBound representing positive/negative infinity, - or LexBoundary representing a specific lexicographical value and inclusivity. - max_lex (Union[InfBound, LexBoundary]): The maximum lexicographical to count up to. - Can be an instance of InfBound representing positive/negative infinity, - or LexBoundary representing a specific lexicographical value and inclusivity. + key (TEncodable): The key of the stream. Returns: - int: The number of members in the specified lexicographical range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. - If `max_lex < min_lex`, `0` is returned. + List[Mapping[bytes, Union[bytes, int, None]]]: A list of mappings, where each mapping represents the + attributes of a consumer group for the stream at `key`. Examples: - >>> await client.zlexcount("my_sorted_set", LexBoundary("c" , is_inclusive=True), InfBound.POS_INF) - 2 # Indicates that there are 2 members with lexicographical values between "c" (inclusive) and positive infinity in the sorted set "my_sorted_set". - >>> await client.zlexcount("my_sorted_set", LexBoundary("c" , is_inclusive=True), LexBoundary("k" , is_inclusive=False)) - 1 # Indicates that there is one member with LexBoundary "c" <= lexicographical value < "k" in the sorted set "my_sorted_set". + >>> await client.xinfo_groups("my_stream") + [ + { + b"name": b"mygroup", + b"consumers": 2, + b"pending": 2, + b"last-delivered-id": b"1638126030001-0", + b"entries-read": 2, # The "entries-read" field was added in Valkey version 7.0.0. + b"lag": 0, # The "lag" field was added in Valkey version 7.0.0. + }, + { + b"name": b"some-other-group", + b"consumers": 1, + b"pending": 0, + b"last-delivered-id": b"1638126028070-0", + b"entries-read": 1, + b"lag": 1, + } + ] + # The list of consumer groups and their attributes for stream "my_stream". """ - min_lex_arg = ( - min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value - ) - max_lex_arg = ( - max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value + return cast( + List[Mapping[bytes, Union[bytes, int, None]]], + await self._execute_command(RequestType.XInfoGroups, [key]), ) + async def xinfo_consumers( + self, + key: TEncodable, + group_name: TEncodable, + ) -> List[Mapping[bytes, Union[bytes, int]]]: + """ + Returns the list of all consumers and their attributes for the given consumer group of the stream stored at + `key`. + + See https://valkey.io/commands/xinfo-consumers for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + + Returns: + List[Mapping[bytes, Union[bytes, int]]]: A list of mappings, where each mapping contains the attributes of a + consumer for the given consumer group of the stream at `key`. + + Examples: + >>> await client.xinfo_consumers("my_stream", "my_group") + [ + { + b"name": b"Alice", + b"pending": 1, + b"idle": 9104628, + b"inactive": 18104698, # The "inactive" field was added in Valkey version 7.2.0. + }, + { + b"name": b"Bob", + b"pending": 1, + b"idle": 83841983, + b"inactive": 993841998, + } + ] + # The list of consumers and their attributes for consumer group "my_group" of stream "my_stream". + """ return cast( - int, - await self._execute_command( - RequestType.ZLexCount, [key, min_lex_arg, max_lex_arg] - ), + List[Mapping[bytes, Union[bytes, int]]], + await self._execute_command(RequestType.XInfoConsumers, [key, group_name]), ) - async def zscore(self, key: str, member: str) -> Optional[float]: + async def xinfo_stream( + self, + key: TEncodable, + ) -> TXInfoStreamResponse: """ - Returns the score of `member` in the sorted set stored at `key`. + Returns information about the stream stored at `key`. To get more detailed information, use `xinfo_stream_full`. - See https://redis.io/commands/zscore/ for more details. + See https://valkey.io/commands/xinfo-stream for more details. Args: - key (str): The key of the sorted set. - member (str): The member whose score is to be retrieved. + key (TEncodable): The key of the stream. Returns: - Optional[float]: The score of the member. - If `member` does not exist in the sorted set, None is returned. - If `key` does not exist, None is returned. + TXInfoStreamResponse: A mapping of stream information for the given `key`. See the example for a sample + response. Examples: - >>> await client.zscore("my_sorted_set", "member") - 10.5 # Indicates that the score of "member" in the sorted set "my_sorted_set" is 10.5. - >>> await client.zscore("my_sorted_set", "non_existing_member") - None + >>> await client.xinfo_stream("my_stream") + { + b"length": 4, + b"radix-tree-keys": 1L, + b"radix-tree-nodes": 2L, + b"last-generated-id": b"1719877599564-0", + b"max-deleted-entry-id": b"0-0", # This field was added in Valkey version 7.0.0. + b"entries-added": 4L, # This field was added in Valkey version 7.0.0. + b"recorded-first-entry-id": b"1719710679916-0", # This field was added in Valkey version 7.0.0. + b"groups": 1L, + b"first-entry": [ + b"1719710679916-0", + [b"foo1", b"bar1", b"foo2", b"bar2"], + ], + b"last-entry": [ + b"1719877599564-0", + [b"field1", b"value1"], + ], + } + # Stream information for "my_stream". Note that "first-entry" and "last-entry" could both be `None` if + # the stream is empty. """ return cast( - Optional[float], - await self._execute_command(RequestType.ZScore, [key, member]), + TXInfoStreamResponse, + await self._execute_command(RequestType.XInfoStream, [key]), ) - async def invoke_script( + async def xinfo_stream_full( self, - script: Script, - keys: Optional[List[str]] = None, - args: Optional[List[str]] = None, - ) -> TResult: + key: TEncodable, + count: Optional[int] = None, + ) -> TXInfoStreamFullResponse: """ - Invokes a Lua script with its keys and arguments. - This method simplifies the process of invoking scripts on a Redis server by using an object that represents a Lua script. - The script loading, argument preparation, and execution will all be handled internally. - If the script has not already been loaded, it will be loaded automatically using the Redis `SCRIPT LOAD` command. - After that, it will be invoked using the Redis `EVALSHA` command. + Returns verbose information about the stream stored at `key`. - See https://redis.io/commands/script-load/ and https://redis.io/commands/evalsha/ for more details. + See https://valkey.io/commands/xinfo-stream for more details. Args: - script (Script): The Lua script to execute. - keys (List[str]): The keys that are used in the script. - args (List[str]): The arguments for the script. + key (TEncodable): The key of the stream. + count (Optional[int]): The number of stream and PEL entries that are returned. A value of `0` means that all + entries will be returned. If not provided, defaults to `10`. Returns: - TResult: a value that depends on the script that was executed. + TXInfoStreamFullResponse: A mapping of detailed stream information for the given `key`. See the example for + a sample response. Examples: - >>> lua_script = Script("return { KEYS[1], ARGV[1] }") - >>> await invoke_script(lua_script, keys=["foo"], args=["bar"] ); - ["foo", "bar"] + >>> await client.xinfo_stream_full("my_stream") + { + b"length": 4, + b"radix-tree-keys": 1L, + b"radix-tree-nodes": 2L, + b"last-generated-id": b"1719877599564-0", + b"max-deleted-entry-id": b"0-0", # This field was added in Valkey version 7.0.0. + b"entries-added": 4L, # This field was added in Valkey version 7.0.0. + b"recorded-first-entry-id": b"1719710679916-0", # This field was added in Valkey version 7.0.0. + b"entries": [ + [ + b"1719710679916-0", + [b"foo1", b"bar1", b"foo2", b"bar2"], + ], + [ + b"1719877599564-0": + [b"field1", b"value1"], + ] + ], + b"groups": [ + { + b"name": b"mygroup", + b"last-delivered-id": b"1719710688676-0", + b"entries-read": 2, # This field was added in Valkey version 7.0.0. + b"lag": 0, # This field was added in Valkey version 7.0.0. + b"pel-count": 2, + b"pending": [ + [ + b"1719710679916-0", + b"Alice", + 1719710707260, + 1, + ], + [ + b"1719710688676-0", + b"Alice", + 1719710718373, + 1, + ], + ], + b"consumers": [ + { + b"name": b"Alice", + b"seen-time": 1719710718373, + b"active-time": 1719710718373, # This field was added in Valkey version 7.2.0. + b"pel-count": 2, + b"pending": [ + [ + b"1719710679916-0", + 1719710707260, + 1 + ], + [ + b"1719710688676-0", + 1719710718373, + 1 + ] + ] + } + ] + } + ] + } + # Detailed stream information for "my_stream". + + Since: Valkey version 6.0.0. + """ + args = [key, "FULL"] + if count is not None: + args.extend(["COUNT", str(count)]) + + return cast( + TXInfoStreamFullResponse, + await self._execute_command(RequestType.XInfoStream, args), + ) + + async def geoadd( + self, + key: TEncodable, + members_geospatialdata: Mapping[TEncodable, GeospatialData], + existing_options: Optional[ConditionalChange] = None, + changed: bool = False, + ) -> int: """ - return await self._execute_script(script.get_hash(), keys, args) + Adds geospatial members with their positions to the specified sorted set stored at `key`. + If a member is already a part of the sorted set, its position is updated. + + See https://valkey.io/commands/geoadd for more details. + + Args: + key (TEncodable): The key of the sorted set. + members_geospatialdata (Mapping[TEncodable, GeospatialData]): A mapping of member names to their corresponding positions. See `GeospatialData`. + The command will report an error when the user attempts to index coordinates outside the specified ranges. + existing_options (Optional[ConditionalChange]): Options for handling existing members. + - NX: Only add new elements. + - XX: Only update existing elements. + changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + + Returns: + int: The number of elements added to the sorted set. + If `changed` is set, returns the number of elements updated in the sorted set. + + Examples: + >>> await client.geoadd("my_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + 2 # Indicates that two elements have been added to the sorted set "my_sorted_set". + >>> await client.geoadd("my_sorted_set", {"Palermo": GeospatialData(14.361389, 38.115556)}, existing_options=ConditionalChange.XX, changed=True) + 1 # Updates the position of an existing member in the sorted set "my_sorted_set". + """ + args = [key] + if existing_options: + args.append(existing_options.value) + + if changed: + args.append("CH") + + members_geospatialdata_list = [ + coord + for member, position in members_geospatialdata.items() + for coord in [str(position.longitude), str(position.latitude), member] + ] + args += members_geospatialdata_list + + return cast( + int, + await self._execute_command(RequestType.GeoAdd, args), + ) + + async def geodist( + self, + key: TEncodable, + member1: TEncodable, + member2: TEncodable, + unit: Optional[GeoUnit] = None, + ) -> Optional[float]: + """ + Returns the distance between two members in the geospatial index stored at `key`. + + See https://valkey.io/commands/geodist for more details. + + Args: + key (TEncodable): The key of the sorted set. + member1 (TEncodable): The name of the first member. + member2 (TEncodable): The name of the second member. + unit (Optional[GeoUnit]): The unit of distance measurement. See `GeoUnit`. + If not specified, the default unit is `METERS`. + + Returns: + Optional[float]: The distance between `member1` and `member2`. + If one or both members do not exist, or if the key does not exist, returns None. + + Examples: + >>> await client.geoadd("my_geo_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + >>> await client.geodist("my_geo_set", "Palermo", "Catania") + 166274.1516 # Indicates the distance between "Palermo" and "Catania" in meters. + >>> await client.geodist("my_geo_set", "Palermo", "Palermo", unit=GeoUnit.KILOMETERS) + 166.2742 # Indicates the distance between "Palermo" and "Palermo" in kilometers. + >>> await client.geodist("my_geo_set", "non-existing", "Palermo", unit=GeoUnit.KILOMETERS) + None # Returns None for non-existing member. + """ + args = [key, member1, member2] + if unit: + args.append(unit.value) + + return cast( + Optional[float], + await self._execute_command(RequestType.GeoDist, args), + ) + + async def geohash( + self, key: TEncodable, members: List[TEncodable] + ) -> List[Optional[bytes]]: + """ + Returns the GeoHash bytes strings representing the positions of all the specified members in the sorted set stored at + `key`. + + See https://valkey.io/commands/geohash for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): The list of members whose GeoHash bytes strings are to be retrieved. + + Returns: + List[Optional[bytes]]: A list of GeoHash bytes strings representing the positions of the specified members stored at `key`. + If a member does not exist in the sorted set, a None value is returned for that member. + + Examples: + >>> await client.geoadd("my_geo_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + >>> await client.geohash("my_geo_sorted_set", ["Palermo", "Catania", "some city]) + ["sqc8b49rny0", "sqdtr74hyu0", None] # Indicates the GeoHash bytes strings for the specified members. + """ + return cast( + List[Optional[bytes]], + await self._execute_command(RequestType.GeoHash, [key] + members), + ) + + async def geopos( + self, + key: TEncodable, + members: List[TEncodable], + ) -> List[Optional[List[float]]]: + """ + Returns the positions (longitude and latitude) of all the given members of a geospatial index in the sorted set stored at + `key`. + + See https://valkey.io/commands/geopos for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): The members for which to get the positions. + + Returns: + List[Optional[List[float]]]: A list of positions (longitude and latitude) corresponding to the given members. + If a member does not exist, its position will be None. + + Example: + >>> await client.geoadd("my_geo_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + >>> await client.geopos("my_geo_sorted_set", ["Palermo", "Catania", "NonExisting"]) + [[13.36138933897018433, 38.11555639549629859], [15.08726745843887329, 37.50266842333162032], None] + """ + return cast( + List[Optional[List[float]]], + await self._execute_command(RequestType.GeoPos, [key] + members), + ) + + async def geosearch( + self, + key: TEncodable, + search_from: Union[str, bytes, GeospatialData], + search_by: Union[GeoSearchByRadius, GeoSearchByBox], + order_by: Optional[OrderBy] = None, + count: Optional[GeoSearchCount] = None, + with_coord: bool = False, + with_dist: bool = False, + with_hash: bool = False, + ) -> List[Union[bytes, List[Union[bytes, float, int, List[float]]]]]: + """ + Searches for members in a sorted set stored at `key` representing geospatial data within a circular or rectangular area. + + See https://valkey.io/commands/geosearch/ for more details. + + Args: + key (TEncodable): The key of the sorted set representing geospatial data. + search_from (Union[str, bytes, GeospatialData]): The location to search from. Can be specified either as a member + from the sorted set or as a geospatial data (see `GeospatialData`). + search_by (Union[GeoSearchByRadius, GeoSearchByBox]): The search criteria. + For circular area search, see `GeoSearchByRadius`. + For rectengal area search, see `GeoSearchByBox`. + order_by (Optional[OrderBy]): Specifies the order in which the results should be returned. + - `ASC`: Sorts items from the nearest to the farthest, relative to the center point. + - `DESC`: Sorts items from the farthest to the nearest, relative to the center point. + If not specified, the results would be unsorted. + count (Optional[GeoSearchCount]): Specifies the maximum number of results to return. See `GeoSearchCount`. + If not specified, return all results. + with_coord (bool): Whether to include coordinates of the returned items. Defaults to False. + with_dist (bool): Whether to include distance from the center in the returned items. + The distance is returned in the same unit as specified for the `search_by` arguments. Defaults to False. + with_hash (bool): Whether to include geohash of the returned items. Defaults to False. + + Returns: + List[Union[bytes, List[Union[bytes, float, int, List[float]]]]]: By default, returns a list of members (locations) names. + If any of `with_coord`, `with_dist` or `with_hash` are True, returns an array of arrays, we're each sub array represents a single item in the following order: + (bytes): The member (location) name. + (float): The distance from the center as a floating point number, in the same unit specified in the radius, if `with_dist` is set to True. + (int): The Geohash integer, if `with_hash` is set to True. + List[float]: The coordinates as a two item [longitude,latitude] array, if `with_coord` is set to True. + + Examples: + >>> await client.geoadd("my_geo_sorted_set", {"edge1": GeospatialData(12.758489, 38.788135), "edge2": GeospatialData(17.241510, 38.788135)}}) + >>> await client.geoadd("my_geo_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + >>> await client.geosearch("my_geo_sorted_set", "Catania", GeoSearchByRadius(175, GeoUnit.MILES), OrderBy.DESC) + ['Palermo', 'Catania'] # Returned the locations names within the radius of 175 miles, with the center being 'Catania' from farthest to nearest. + >>> await client.geosearch("my_geo_sorted_set", GeospatialData(15, 37), GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), OrderBy.DESC, with_coord=true, with_dist=true, with_hash=true) + [ + [ + b"Catania", + [56.4413, 3479447370796909, [15.087267458438873, 37.50266842333162]], + ], + [ + b"Palermo", + [190.4424, 3479099956230698, [13.361389338970184, 38.1155563954963]], + ], + [ + b"edge2", + [279.7403, 3481342659049484, [17.241510450839996, 38.78813451624225]], + ], + [ + b"edge1", + [279.7405, 3479273021651468, [12.75848776102066, 38.78813451624225]], + ], + ] # Returns locations within the square box of 400 km, with the center being a specific point, from nearest to farthest with the dist, hash and coords. + + Since: Valkey version 6.2.0. + """ + args = _create_geosearch_args( + [key], + search_from, + search_by, + order_by, + count, + with_coord, + with_dist, + with_hash, + ) + + return cast( + List[Union[bytes, List[Union[bytes, float, int, List[float]]]]], + await self._execute_command(RequestType.GeoSearch, args), + ) + + async def geosearchstore( + self, + destination: TEncodable, + source: TEncodable, + search_from: Union[str, bytes, GeospatialData], + search_by: Union[GeoSearchByRadius, GeoSearchByBox], + count: Optional[GeoSearchCount] = None, + store_dist: bool = False, + ) -> int: + """ + Searches for members in a sorted set stored at `key` representing geospatial data within a circular or rectangular area and stores the result in `destination`. + If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + + To get the result directly, see `geosearch`. + + Note: + When in cluster mode, both `source` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key to store the search results. + source (TEncodable): The key of the sorted set representing geospatial data to search from. + search_from (Union[str, bytes, GeospatialData]): The location to search from. Can be specified either as a member + from the sorted set or as a geospatial data (see `GeospatialData`). + search_by (Union[GeoSearchByRadius, GeoSearchByBox]): The search criteria. + For circular area search, see `GeoSearchByRadius`. + For rectangular area search, see `GeoSearchByBox`. + count (Optional[GeoSearchCount]): Specifies the maximum number of results to store. See `GeoSearchCount`. + If not specified, stores all results. + store_dist (bool): Determines what is stored as the sorted set score. Defaults to False. + - If set to False, the geohash of the location will be stored as the sorted set score. + - If set to True, the distance from the center of the shape (circle or box) will be stored as the sorted set score. + The distance is represented as a floating-point number in the same unit specified for that shape. + + Returns: + int: The number of elements in the resulting sorted set stored at `destination`. + + Examples: + >>> await client.geoadd("my_geo_sorted_set", {"Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669)}) + >>> await client.geosearchstore("my_dest_sorted_set", "my_geo_sorted_set", "Catania", GeoSearchByRadius(175, GeoUnit.MILES)) + 2 # Number of elements stored in "my_dest_sorted_set". + >>> await client.zrange_withscores("my_dest_sorted_set", RangeByIndex(0, -1)) + {b"Palermo": 3479099956230698.0, b"Catania": 3479447370796909.0} # The elements within te search area, with their geohash as score. + >>> await client.geosearchstore("my_dest_sorted_set", "my_geo_sorted_set", GeospatialData(15, 37), GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), store_dist=True) + 2 # Number of elements stored in "my_dest_sorted_set", with distance as score. + >>> await client.zrange_withscores("my_dest_sorted_set", RangeByIndex(0, -1)) + {b"Catania": 56.4412578701582, b"Palermo": 190.44242984775784} # The elements within te search area, with the distance as score. + + Since: Valkey version 6.2.0. + """ + args = _create_geosearch_args( + [destination, source], + search_from, + search_by, + None, + count, + False, + False, + False, + store_dist, + ) + + return cast( + int, + await self._execute_command(RequestType.GeoSearchStore, args), + ) + + async def zadd( + self, + key: TEncodable, + members_scores: Mapping[TEncodable, float], + existing_options: Optional[ConditionalChange] = None, + update_condition: Optional[UpdateOptions] = None, + changed: bool = False, + ) -> int: + """ + Adds members with their scores to the sorted set stored at `key`. + If a member is already a part of the sorted set, its score is updated. + + See https://valkey.io/commands/zadd/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + members_scores (Mapping[TEncodable, float]): A mapping of members to their corresponding scores. + existing_options (Optional[ConditionalChange]): Options for handling existing members. + - NX: Only add new elements. + - XX: Only update existing elements. + update_condition (Optional[UpdateOptions]): Options for updating scores. + - GT: Only update scores greater than the current values. + - LT: Only update scores less than the current values. + changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + + Returns: + int: The number of elements added to the sorted set. + If `changed` is set, returns the number of elements updated in the sorted set. + + Examples: + >>> await client.zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) + 2 # Indicates that two elements have been added to the sorted set "my_sorted_set." + >>> await client.zadd("existing_sorted_set", {"member1": 15.0, "member2": 5.5}, existing_options=ConditionalChange.XX, changed=True) + 2 # Updates the scores of two existing members in the sorted set "existing_sorted_set." + """ + args = [key] + if existing_options: + args.append(existing_options.value) + + if update_condition: + args.append(update_condition.value) + + if changed: + args.append("CH") + + if existing_options and update_condition: + if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: + raise ValueError( + "The GT, LT and NX options are mutually exclusive. " + f"Cannot choose both {update_condition.value} and NX." + ) + + members_scores_list = [ + str(item) for pair in members_scores.items() for item in pair[::-1] + ] + args += members_scores_list + + return cast( + int, + await self._execute_command(RequestType.ZAdd, args), + ) + + async def zadd_incr( + self, + key: TEncodable, + member: TEncodable, + increment: float, + existing_options: Optional[ConditionalChange] = None, + update_condition: Optional[UpdateOptions] = None, + ) -> Optional[float]: + """ + Increments the score of member in the sorted set stored at `key` by `increment`. + If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). + If `key` does not exist, a new sorted set with the specified member as its sole member is created. + + See https://valkey.io/commands/zadd/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): A member in the sorted set to increment. + increment (float): The score to increment the member. + existing_options (Optional[ConditionalChange]): Options for handling the member's existence. + - NX: Only increment a member that doesn't exist. + - XX: Only increment an existing member. + update_condition (Optional[UpdateOptions]): Options for updating the score. + - GT: Only increment the score of the member if the new score will be greater than the current score. + - LT: Only increment (decrement) the score of the member if the new score will be less than the current score. + + Returns: + Optional[float]: The score of the member. + If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. + + Examples: + >>> await client.zadd_incr("my_sorted_set", member , 5.0) + 5.0 + >>> await client.zadd_incr("existing_sorted_set", member , "3.0" , UpdateOptions.LESS_THAN) + None + """ + args = [key] + if existing_options: + args.append(existing_options.value) + + if update_condition: + args.append(update_condition.value) + + args.append("INCR") + + if existing_options and update_condition: + if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: + raise ValueError( + "The GT, LT and NX options are mutually exclusive. " + f"Cannot choose both {update_condition.value} and NX." + ) + + args += [str(increment), member] + return cast( + Optional[float], + await self._execute_command(RequestType.ZAdd, args), + ) + + async def zcard(self, key: TEncodable) -> int: + """ + Returns the cardinality (number of elements) of the sorted set stored at `key`. + + See https://valkey.io/commands/zcard/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + + Returns: + int: The number of elements in the sorted set. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + + Examples: + >>> await client.zcard("my_sorted_set") + 3 # Indicates that there are 3 elements in the sorted set "my_sorted_set". + >>> await client.zcard("non_existing_key") + 0 + """ + return cast(int, await self._execute_command(RequestType.ZCard, [key])) + + async def zcount( + self, + key: TEncodable, + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], + ) -> int: + """ + Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. + + See https://valkey.io/commands/zcount/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + + Returns: + int: The number of members in the specified score range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + If `max_score` < `min_score`, 0 is returned. + + Examples: + >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , InfBound.POS_INF) + 2 # Indicates that there are 2 members with scores between 5.0 (not exclusive) and +inf in the sorted set "my_sorted_set". + >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , ScoreBoundary(10.0 , is_inclusive=false)) + 1 # Indicates that there is one ScoreBoundary with 5.0 < score <= 10.0 in the sorted set "my_sorted_set". + """ + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value + ) + return cast( + int, + await self._execute_command( + RequestType.ZCount, [key, score_min, score_max] + ), + ) + + async def zincrby( + self, key: TEncodable, increment: float, member: TEncodable + ) -> float: + """ + Increments the score of `member` in the sorted set stored at `key` by `increment`. + If `member` does not exist in the sorted set, it is added with `increment` as its score. + If `key` does not exist, a new sorted set is created with the specified member as its sole member. + + See https://valkey.io/commands/zincrby/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + increment (float): The score increment. + member (TEncodable): A member of the sorted set. + + Returns: + float: The new score of `member`. + + Examples: + >>> await client.zadd("my_sorted_set", {"member": 10.5, "member2": 8.2}) + >>> await client.zincrby("my_sorted_set", 1.2, "member") + 11.7 # The member existed in the set before score was altered, the new score is 11.7. + >>> await client.zincrby("my_sorted_set", -1.7, "member") + 10.0 # Negetive increment, decrements the score. + >>> await client.zincrby("my_sorted_set", 5.5, "non_existing_member") + 5.5 # A new memeber is added to the sorted set with the score being 5.5. + """ + return cast( + float, + await self._execute_command( + RequestType.ZIncrBy, [key, str(increment), member] + ), + ) + + async def zpopmax( + self, key: TEncodable, count: Optional[int] = None + ) -> Mapping[bytes, float]: + """ + Removes and returns the members with the highest scores from the sorted set stored at `key`. + If `count` is provided, up to `count` members with the highest scores are removed and returned. + Otherwise, only one member with the highest score is removed and returned. + + See https://valkey.io/commands/zpopmax for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. + If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. + + Returns: + Mapping[bytes, float]: A map of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. + If `key` doesn't exist, it will be treated as an empy sorted set and the command returns an empty map. + + Examples: + >>> await client.zpopmax("my_sorted_set") + {b'member1': 10.0} # Indicates that 'member1' with a score of 10.0 has been removed from the sorted set. + >>> await client.zpopmax("my_sorted_set", 2) + {b'member2': 8.0, b'member3': 7.5} # Indicates that 'member2' with a score of 8.0 and 'member3' with a score of 7.5 have been removed from the sorted set. + """ + return cast( + Mapping[bytes, float], + await self._execute_command( + RequestType.ZPopMax, [key, str(count)] if count else [key] + ), + ) + + async def bzpopmax( + self, keys: List[TEncodable], timeout: float + ) -> Optional[List[Union[bytes, float]]]: + """ + Pops the member with the highest score from the first non-empty sorted set, with the given keys being checked in + the order that they are given. Blocks the connection when there are no members to remove from any of the given + sorted sets. + + When in cluster mode, all keys must map to the same hash slot. + + `BZPOPMAX` is the blocking variant of `ZPOPMAX`. + + `BZPOPMAX` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/bzpopmax for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + timeout (float): The number of seconds to wait for a blocking operation to complete. + A value of 0 will block indefinitely. + + Returns: + Optional[List[Union[bytes, float]]]: An array containing the key where the member was popped out, the member itself, + and the member score. If no member could be popped and the `timeout` expired, returns None. + + Examples: + >>> await client.zadd("my_sorted_set1", {"member1": 10.0, "member2": 5.0}) + 2 # Two elements have been added to the sorted set at "my_sorted_set1". + >>> await client.bzpopmax(["my_sorted_set1", "my_sorted_set2"], 0.5) + [b'my_sorted_set1', b'member1', 10.0] # "member1" with a score of 10.0 has been removed from "my_sorted_set1". + """ + return cast( + Optional[List[Union[bytes, float]]], + await self._execute_command(RequestType.BZPopMax, keys + [str(timeout)]), + ) + + async def zpopmin( + self, key: TEncodable, count: Optional[int] = None + ) -> Mapping[bytes, float]: + """ + Removes and returns the members with the lowest scores from the sorted set stored at `key`. + If `count` is provided, up to `count` members with the lowest scores are removed and returned. + Otherwise, only one member with the lowest score is removed and returned. + + See https://valkey.io/commands/zpopmin for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. + If `count` is higher than the sorted set's cardinality, returns all members and their scores. + + Returns: + Mapping[bytes, float]: A map of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. + If `key` doesn't exist, it will be treated as an empy sorted set and the command returns an empty map. + + Examples: + >>> await client.zpopmin("my_sorted_set") + {b'member1': 5.0} # Indicates that 'member1' with a score of 5.0 has been removed from the sorted set. + >>> await client.zpopmin("my_sorted_set", 2) + {b'member3': 7.5 , b'member2': 8.0} # Indicates that 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set. + """ + args: List[TEncodable] = [key, str(count)] if count else [key] + return cast( + Mapping[bytes, float], + await self._execute_command(RequestType.ZPopMin, args), + ) + + async def bzpopmin( + self, keys: List[TEncodable], timeout: float + ) -> Optional[List[Union[bytes, float]]]: + """ + Pops the member with the lowest score from the first non-empty sorted set, with the given keys being checked in + the order that they are given. Blocks the connection when there are no members to remove from any of the given + sorted sets. + + When in cluster mode, all keys must map to the same hash slot. + + `BZPOPMIN` is the blocking variant of `ZPOPMIN`. + + `BZPOPMIN` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/bzpopmin for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + timeout (float): The number of seconds to wait for a blocking operation to complete. + A value of 0 will block indefinitely. + + Returns: + Optional[List[Union[bytes, float]]]: An array containing the key where the member was popped out, the member itself, + and the member score. If no member could be popped and the `timeout` expired, returns None. + + Examples: + >>> await client.zadd("my_sorted_set1", {"member1": 10.0, "member2": 5.0}) + 2 # Two elements have been added to the sorted set at "my_sorted_set1". + >>> await client.bzpopmin(["my_sorted_set1", "my_sorted_set2"], 0.5) + [b'my_sorted_set1', b'member2', 5.0] # "member2" with a score of 5.0 has been removed from "my_sorted_set1". + """ + args: List[TEncodable] = keys + [str(timeout)] + return cast( + Optional[List[Union[bytes, float]]], + await self._execute_command(RequestType.BZPopMin, args), + ) + + async def zrange( + self, + key: TEncodable, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> List[bytes]: + """ + Returns the specified range of elements in the sorted set stored at `key`. + + ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + + See https://valkey.io/commands/zrange/ for more details. + + To get the elements with their scores, see zrange_withscores. + + Args: + key (TEncodable): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + List[bytes]: A list of elements within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + + Examples: + >>> await client.zrange("my_sorted_set", RangeByIndex(0, -1)) + [b'member1', b'member2', b'member3'] # Returns all members in ascending order. + >>> await client.zrange("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) + [b'member2', b'member3'] # Returns members with scores within the range of negative infinity to 3, in ascending order. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=False) + + return cast(List[bytes], await self._execute_command(RequestType.ZRange, args)) + + async def zrange_withscores( + self, + key: TEncodable, + range_query: Union[RangeByIndex, RangeByScore], + reverse: bool = False, + ) -> Mapping[bytes, float]: + """ + Returns the specified range of elements with their scores in the sorted set stored at `key`. + Similar to ZRANGE but with a WITHSCORE flag. + + See https://valkey.io/commands/zrange/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + Mapping[bytes , float]: A map of elements and their scores within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + + Examples: + >>> await client.zrange_withscores("my_sorted_set", RangeByScore(ScoreBoundary(10), ScoreBoundary(20))) + {b'member1': 10.5, b'member2': 15.2} # Returns members with scores between 10 and 20 with their scores. + >>> await client.zrange_withscores("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) + {b'member4': -2.0, b'member7': 1.5} # Returns members with with scores within the range of negative infinity to 3, with their scores. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=True) + + return cast( + Mapping[bytes, float], await self._execute_command(RequestType.ZRange, args) + ) + + async def zrangestore( + self, + destination: TEncodable, + source: TEncodable, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> int: + """ + Stores a specified range of elements from the sorted set at `source`, into a new sorted set at `destination`. If + `destination` doesn't exist, a new sorted set is created; if it exists, it's overwritten. + + ZRANGESTORE can perform different types of range queries: by index (rank), by the score, or by lexicographical + order. + + See https://valkey.io/commands/zrangestore for more details. + + Note: + When in Cluster mode, `source` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key for the destination sorted set. + source (TEncodable): The key of the source sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + int: The number of elements in the resulting sorted set. + + Examples: + >>> await client.zrangestore("destination_key", "my_sorted_set", RangeByIndex(0, 2), True) + 3 # The 3 members with the highest scores from "my_sorted_set" were stored in the sorted set at "destination_key". + >>> await client.zrangestore("destination_key", "my_sorted_set", RangeByScore(InfBound.NEG_INF, ScoreBoundary(3))) + 2 # The 2 members with scores between negative infinity and 3 (inclusive) from "my_sorted_set" were stored in the sorted set at "destination_key". + """ + args = _create_zrange_args(source, range_query, reverse, False, destination) + + return cast(int, await self._execute_command(RequestType.ZRangeStore, args)) + + async def zrank( + self, + key: TEncodable, + member: TEncodable, + ) -> Optional[int]: + """ + Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. + + See https://valkey.io/commands/zrank for more details. + + To get the rank of `member` with its score, see `zrank_withscore`. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Returns: + Optional[int]: The rank of `member` in the sorted set. + If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + + Examples: + >>> await client.zrank("my_sorted_set", "member2") + 1 # Indicates that "member2" has the second-lowest score in the sorted set "my_sorted_set". + >>> await client.zrank("my_sorted_set", "non_existing_member") + None # Indicates that "non_existing_member" is not present in the sorted set "my_sorted_set". + """ + return cast( + Optional[int], await self._execute_command(RequestType.ZRank, [key, member]) + ) + + async def zrank_withscore( + self, + key: TEncodable, + member: TEncodable, + ) -> Optional[List[Union[int, float]]]: + """ + Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. + + See https://valkey.io/commands/zrank for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Returns: + Optional[List[Union[int, float]]]: A list containing the rank and score of `member` in the sorted set. + If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + + Examples: + >>> await client.zrank_withscore("my_sorted_set", "member2") + [1 , 6.0] # Indicates that "member2" with score 6.0 has the second-lowest score in the sorted set "my_sorted_set". + >>> await client.zrank_withscore("my_sorted_set", "non_existing_member") + None # Indicates that "non_existing_member" is not present in the sorted set "my_sorted_set". + + Since: Valkey version 7.2.0. + """ + return cast( + Optional[List[Union[int, float]]], + await self._execute_command(RequestType.ZRank, [key, member, "WITHSCORE"]), + ) + + async def zrevrank(self, key: TEncodable, member: TEncodable) -> Optional[int]: + """ + Returns the rank of `member` in the sorted set stored at `key`, where scores are ordered from the highest to + lowest, starting from `0`. + + To get the rank of `member` with its score, see `zrevrank_withscore`. + + See https://valkey.io/commands/zrevrank for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Returns: + Optional[int]: The rank of `member` in the sorted set, where ranks are ordered from high to low based on scores. + If `key` doesn't exist, or if `member` is not present in the set, `None` will be returned. + + Examples: + >>> await client.zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2, "member3": 9.6}) + >>> await client.zrevrank("my_sorted_set", "member2") + 2 # "member2" has the third-highest score in the sorted set "my_sorted_set" + """ + return cast( + Optional[int], + await self._execute_command(RequestType.ZRevRank, [key, member]), + ) + + async def zrevrank_withscore( + self, key: TEncodable, member: TEncodable + ) -> Optional[List[Union[int, float]]]: + """ + Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the + highest to lowest, starting from `0`. + + See https://valkey.io/commands/zrevrank for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Returns: + Optional[List[Union[int, float]]]: A list containing the rank (as `int`) and score (as `float`) of `member` + in the sorted set, where ranks are ordered from high to low based on scores. + If `key` doesn't exist, or if `member` is not present in the set, `None` will be returned. + + Examples: + >>> await client.zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2, "member3": 9.6}) + >>> await client.zrevrank("my_sorted_set", "member2") + [2, 8.2] # "member2" with score 8.2 has the third-highest score in the sorted set "my_sorted_set" + + Since: Valkey version 7.2.0. + """ + return cast( + Optional[List[Union[int, float]]], + await self._execute_command( + RequestType.ZRevRank, [key, member, "WITHSCORE"] + ), + ) + + async def zrem( + self, + key: TEncodable, + members: List[TEncodable], + ) -> int: + """ + Removes the specified members from the sorted set stored at `key`. + Specified members that are not a member of this set are ignored. + + See https://valkey.io/commands/zrem/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): A list of members to remove from the sorted set. + + Returns: + int: The number of members that were removed from the sorted set, not including non-existing members. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + + Examples: + >>> await client.zrem("my_sorted_set", ["member1", "member2"]) + 2 # Indicates that two members have been removed from the sorted set "my_sorted_set." + >>> await client.zrem("non_existing_sorted_set", ["member1", "member2"]) + 0 # Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. + """ + return cast( + int, + await self._execute_command(RequestType.ZRem, [key] + members), + ) + + async def zremrangebyscore( + self, + key: TEncodable, + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], + ) -> int: + """ + Removes all elements in the sorted set stored at `key` with a score between `min_score` and `max_score`. + + See https://valkey.io/commands/zremrangebyscore/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to remove from. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to remove up to. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + Returns: + int: The number of members that were removed from the sorted set. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + If `min_score` is greater than `max_score`, 0 is returned. + + Examples: + >>> await client.zremrangebyscore("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , InfBound.POS_INF) + 2 # Indicates that 2 members with scores between 5.0 (not exclusive) and +inf have been removed from the sorted set "my_sorted_set". + >>> await client.zremrangebyscore("non_existing_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , ScoreBoundary(10.0 , is_inclusive=false)) + 0 # Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. + """ + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value + ) + + return cast( + int, + await self._execute_command( + RequestType.ZRemRangeByScore, [key, score_min, score_max] + ), + ) + + async def zremrangebylex( + self, + key: TEncodable, + min_lex: Union[InfBound, LexBoundary], + max_lex: Union[InfBound, LexBoundary], + ) -> int: + """ + Removes all elements in the sorted set stored at `key` with a lexicographical order between `min_lex` and + `max_lex`. + + See https://valkey.io/commands/zremrangebylex/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_lex (Union[InfBound, LexBoundary]): The minimum bound of the lexicographical range. + Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` + representing a specific lex and inclusivity. + max_lex (Union[InfBound, LexBoundary]): The maximum bound of the lexicographical range. + Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` + representing a specific lex and inclusivity. + + Returns: + int: The number of members that were removed from the sorted set. + If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. + If `min_lex` is greater than `max_lex`, `0` is returned. + + Examples: + >>> await client.zremrangebylex("my_sorted_set", LexBoundary("a", is_inclusive=False), LexBoundary("e")) + 4 # Indicates that 4 members, with lexicographical values ranging from "a" (exclusive) to "e" (inclusive), have been removed from "my_sorted_set". + >>> await client.zremrangebylex("non_existing_sorted_set", InfBound.NEG_INF, LexBoundary("e")) + 0 # Indicates that no members were removed as the sorted set "non_existing_sorted_set" does not exist. + """ + min_lex_arg = ( + min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value + ) + max_lex_arg = ( + max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value + ) + + return cast( + int, + await self._execute_command( + RequestType.ZRemRangeByLex, [key, min_lex_arg, max_lex_arg] + ), + ) + + async def zremrangebyrank( + self, + key: TEncodable, + start: int, + end: int, + ) -> int: + """ + Removes all elements in the sorted set stored at `key` with rank between `start` and `end`. + Both `start` and `end` are zero-based indexes with 0 being the element with the lowest score. + These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. + + See https://valkey.io/commands/zremrangebyrank/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + start (int): The starting point of the range. + end (int): The end of the range. + + Returns: + int: The number of elements that were removed. + If `start` exceeds the end of the sorted set, or if `start` is greater than `end`, `0` is returned. + If `end` exceeds the actual end of the sorted set, the range will stop at the actual end of the sorted set. + If `key` does not exist, `0` is returned. + + Examples: + >>> await client.zremrangebyrank("my_sorted_set", 0, 4) + 5 # Indicates that 5 elements, with ranks ranging from 0 to 4 (inclusive), have been removed from "my_sorted_set". + >>> await client.zremrangebyrank("my_sorted_set", 0, 4) + 0 # Indicates that nothing was removed. + """ + return cast( + int, + await self._execute_command( + RequestType.ZRemRangeByRank, [key, str(start), str(end)] + ), + ) + + async def zlexcount( + self, + key: TEncodable, + min_lex: Union[InfBound, LexBoundary], + max_lex: Union[InfBound, LexBoundary], + ) -> int: + """ + Returns the number of members in the sorted set stored at `key` with lexicographical values between `min_lex` and `max_lex`. + + See https://valkey.io/commands/zlexcount/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_lex (Union[InfBound, LexBoundary]): The minimum lexicographical value to count from. + Can be an instance of InfBound representing positive/negative infinity, + or LexBoundary representing a specific lexicographical value and inclusivity. + max_lex (Union[InfBound, LexBoundary]): The maximum lexicographical to count up to. + Can be an instance of InfBound representing positive/negative infinity, + or LexBoundary representing a specific lexicographical value and inclusivity. + + Returns: + int: The number of members in the specified lexicographical range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. + If `max_lex < min_lex`, `0` is returned. + + Examples: + >>> await client.zlexcount("my_sorted_set", LexBoundary("c" , is_inclusive=True), InfBound.POS_INF) + 2 # Indicates that there are 2 members with lexicographical values between "c" (inclusive) and positive infinity in the sorted set "my_sorted_set". + >>> await client.zlexcount("my_sorted_set", LexBoundary("c" , is_inclusive=True), LexBoundary("k" , is_inclusive=False)) + 1 # Indicates that there is one member with LexBoundary "c" <= lexicographical value < "k" in the sorted set "my_sorted_set". + """ + min_lex_arg = ( + min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value + ) + max_lex_arg = ( + max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value + ) + + return cast( + int, + await self._execute_command( + RequestType.ZLexCount, [key, min_lex_arg, max_lex_arg] + ), + ) + + async def zscore(self, key: TEncodable, member: TEncodable) -> Optional[float]: + """ + Returns the score of `member` in the sorted set stored at `key`. + + See https://valkey.io/commands/zscore/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose score is to be retrieved. + + Returns: + Optional[float]: The score of the member. + If `member` does not exist in the sorted set, None is returned. + If `key` does not exist, None is returned. + + Examples: + >>> await client.zscore("my_sorted_set", "member") + 10.5 # Indicates that the score of "member" in the sorted set "my_sorted_set" is 10.5. + >>> await client.zscore("my_sorted_set", "non_existing_member") + None + """ + return cast( + Optional[float], + await self._execute_command(RequestType.ZScore, [key, member]), + ) + + async def zmscore( + self, + key: TEncodable, + members: List[TEncodable], + ) -> List[Optional[float]]: + """ + Returns the scores associated with the specified `members` in the sorted set stored at `key`. + + See https://valkey.io/commands/zmscore for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): A list of members in the sorted set. + + Returns: + List[Optional[float]]: A list of scores corresponding to `members`. + If a member does not exist in the sorted set, the corresponding value in the list will be None. + + Examples: + >>> await client.zmscore("my_sorted_set", ["one", "non_existent_member", "three"]) + [1.0, None, 3.0] + """ + return cast( + List[Optional[float]], + await self._execute_command(RequestType.ZMScore, [key] + members), + ) + + async def zdiff(self, keys: List[TEncodable]) -> List[bytes]: + """ + Returns the difference between the first sorted set and all the successive sorted sets. + To get the elements with their scores, see `zdiff_withscores`. + + When in Cluster mode, all keys must map to the same hash slot. + + See https://valkey.io/commands/zdiff for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Returns: + List[bytes]: A list of elements representing the difference between the sorted sets. + If the first key does not exist, it is treated as an empty sorted set, and the command returns an + empty list. + + Examples: + >>> await client.zadd("sorted_set1", {"element1":1.0, "element2": 2.0, "element3": 3.0}) + >>> await client.zadd("sorted_set2", {"element2": 2.0}) + >>> await client.zadd("sorted_set3", {"element3": 3.0}) + >>> await client.zdiff("sorted_set1", "sorted_set2", "sorted_set3") + [b"element1"] # Indicates that "element1" is in "sorted_set1" but not "sorted_set2" or "sorted_set3". + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + return cast( + List[bytes], + await self._execute_command(RequestType.ZDiff, args), + ) + + async def zdiff_withscores(self, keys: List[TEncodable]) -> Mapping[bytes, float]: + """ + Returns the difference between the first sorted set and all the successive sorted sets, with the associated scores. + When in Cluster mode, all keys must map to the same hash slot. + + See https://valkey.io/commands/zdiff for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Returns: + Mapping[bytes, float]: A mapping of elements and their scores representing the difference between the sorted + sets. + If the first `key` does not exist, it is treated as an empty sorted set, and the command returns an + empty list. + + Examples: + >>> await client.zadd("sorted_set1", {"element1":1.0, "element2": 2.0, "element3": 3.0}) + >>> await client.zadd("sorted_set2", {"element2": 2.0}) + >>> await client.zadd("sorted_set3", {"element3": 3.0}) + >>> await client.zdiff_withscores("sorted_set1", "sorted_set2", "sorted_set3") + {b"element1": 1.0} # Indicates that "element1" is in "sorted_set1" but not "sorted_set2" or "sorted_set3". + """ + return cast( + Mapping[bytes, float], + await self._execute_command( + RequestType.ZDiff, [str(len(keys))] + keys + ["WITHSCORES"] + ), + ) + + async def zdiffstore(self, destination: TEncodable, keys: List[TEncodable]) -> int: + """ + Calculates the difference between the first sorted set and all the successive sorted sets at `keys` and stores + the difference as a sorted set to `destination`, overwriting it if it already exists. Non-existent keys are + treated as empty sets. + See https://valkey.io/commands/zdiffstore for more details. + + Note: + When in Cluster mode, all keys in `keys` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key for the resulting sorted set. + keys (List[TEncodable]): The keys of the sorted sets to compare. + + Returns: + int: The number of members in the resulting sorted set stored at `destination`. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + 2 # Indicates that two elements have been added to the sorted set at "key1". + >>> await client.zadd("key2", {"member1": 10.5}) + 1 # Indicates that one element has been added to the sorted set at "key2". + >>> await client.zdiffstore("my_sorted_set", ["key1", "key2"]) + 1 # One member exists in "key1" but not "key2", and this member was stored in "my_sorted_set". + >>> await client.zrange("my_sorted_set", RangeByIndex(0, -1)) + ['member2'] # "member2" is now stored in "my_sorted_set" + """ + return cast( + int, + await self._execute_command( + RequestType.ZDiffStore, [destination, str(len(keys))] + keys + ), + ) + + async def zinter( + self, + keys: List[TEncodable], + ) -> List[bytes]: + """ + Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements. + To get the scores as well, see `zinter_withscores`. + To store the result in a key as a sorted set, see `zinterstore`. + + When in cluster mode, all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zinter/ for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Returns: + List[bytes]: The resulting array of intersecting elements. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("key2", {"member1": 9.5}) + >>> await client.zinter(["key1", "key2"]) + [b'member1'] + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + return cast( + List[bytes], + await self._execute_command(RequestType.ZInter, args), + ) + + async def zinter_withscores( + self, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> Mapping[bytes, float]: + """ + Computes the intersection of sorted sets given by the specified `keys` and returns a sorted set of intersecting elements with scores. + To get the elements only, see `zinter`. + To store the result in a key as a sorted set, see `zinterstore`. + + When in cluster mode, all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zinter/ for more details. + + Args: + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Returns: + Mapping[bytes, float]: The resulting sorted set with scores. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("key2", {"member1": 9.5}) + >>> await client.zinter_withscores(["key1", "key2"]) + {b'member1': 20} # "member1" with score of 20 is the result + >>> await client.zinter_withscores(["key1", "key2"], AggregationType.MAX) + {b'member1': 10.5} # "member1" with score of 10.5 is the result. + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type) + args.append("WITHSCORES") + return cast( + Mapping[bytes, float], + await self._execute_command(RequestType.ZInter, args), + ) + + async def zinterstore( + self, + destination: TEncodable, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> int: + """ + Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. + If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + To get the result directly, see `zinter_withscores`. + + When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zinterstore/ for more details. + + Args: + destination (TEncodable): The key of the destination sorted set. + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Returns: + int: The number of elements in the resulting sorted set stored at `destination`. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("key2", {"member1": 9.5}) + >>> await client.zinterstore("my_sorted_set", ["key1", "key2"]) + 1 # Indicates that the sorted set "my_sorted_set" contains one element. + >>> await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) + {b'member1': 20} # "member1" is now stored in "my_sorted_set" with score of 20. + >>> await client.zinterstore("my_sorted_set", ["key1", "key2"], AggregationType.MAX) + 1 # Indicates that the sorted set "my_sorted_set" contains one element, and its score is the maximum score between the sets. + >>> await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) + {b'member1': 10.5} # "member1" is now stored in "my_sorted_set" with score of 10.5. + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type, destination) + return cast( + int, + await self._execute_command(RequestType.ZInterStore, args), + ) + + async def zunion( + self, + keys: List[TEncodable], + ) -> List[bytes]: + """ + Computes the union of sorted sets given by the specified `keys` and returns a list of union elements. + To get the scores as well, see `zunion_withscores`. + To store the result in a key as a sorted set, see `zunionstore`. + + When in cluster mode, all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zunion/ for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Returns: + List[bytes]: The resulting array of union elements. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("key2", {"member1": 9.5}) + >>> await client.zunion(["key1", "key2"]) + [b'member1', b'member2'] + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + return cast( + List[bytes], + await self._execute_command(RequestType.ZUnion, args), + ) + + async def zunion_withscores( + self, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> Mapping[bytes, float]: + """ + Computes the union of sorted sets given by the specified `keys` and returns a sorted set of union elements with scores. + To get the elements only, see `zunion`. + To store the result in a key as a sorted set, see `zunionstore`. + + When in cluster mode, all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zunion/ for more details. + + Args: + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Returns: + Mapping[bytes, float]: The resulting sorted set with scores. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("key2", {"member1": 9.5}) + >>> await client.zunion_withscores(["key1", "key2"]) + {b'member1': 20, b'member2': 8.2} + >>> await client.zunion_withscores(["key1", "key2"], AggregationType.MAX) + {b'member1': 10.5, b'member2': 8.2} + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type) + args.append("WITHSCORES") + return cast( + Mapping[bytes, float], + await self._execute_command(RequestType.ZUnion, args), + ) + + async def zunionstore( + self, + destination: TEncodable, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> int: + """ + Computes the union of sorted sets given by the specified `keys` and stores the result in `destination`. + If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + To get the result directly, see `zunion_withscores`. + + When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zunionstore/ for more details. + + Args: + destination (TEncodable): The key of the destination sorted set. + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Returns: + int: The number of elements in the resulting sorted set stored at `destination`. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("key2", {"member1": 9.5}) + >>> await client.zunionstore("my_sorted_set", ["key1", "key2"]) + 2 # Indicates that the sorted set "my_sorted_set" contains two elements. + >>> await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) + {b'member1': 20, b'member2': 8.2} + >>> await client.zunionstore("my_sorted_set", ["key1", "key2"], AggregationType.MAX) + 2 # Indicates that the sorted set "my_sorted_set" contains two elements, and each score is the maximum score between the sets. + >>> await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) + {b'member1': 10.5, b'member2': 8.2} + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type, destination) + return cast( + int, + await self._execute_command(RequestType.ZUnionStore, args), + ) + + async def zrandmember(self, key: TEncodable) -> Optional[bytes]: + """ + Returns a random member from the sorted set stored at 'key'. + + See https://valkey.io/commands/zrandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + + Returns: + Optional[bytes]: A random member from the sorted set. + If the sorted set does not exist or is empty, the response will be None. + + Examples: + >>> await client.zadd("my_sorted_set", {"member1": 1.0, "member2": 2.0}) + >>> await client.zrandmember("my_sorted_set") + b"member1" # "member1" is a random member of "my_sorted_set". + >>> await client.zrandmember("non_existing_sorted_set") + None # "non_existing_sorted_set" is not an existing key, so None was returned. + """ + args: List[TEncodable] = [key] + return cast( + Optional[bytes], + await self._execute_command(RequestType.ZRandMember, [key]), + ) + + async def zrandmember_count(self, key: TEncodable, count: int) -> List[bytes]: + """ + Retrieves up to the absolute value of `count` random members from the sorted set stored at 'key'. + + See https://valkey.io/commands/zrandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (int): The number of members to return. + If `count` is positive, returns unique members. + If `count` is negative, allows for duplicates members. + + Returns: + List[bytes]: A list of members from the sorted set. + If the sorted set does not exist or is empty, the response will be an empty list. + + Examples: + >>> await client.zadd("my_sorted_set", {"member1": 1.0, "member2": 2.0}) + >>> await client.zrandmember("my_sorted_set", -3) + [b"member1", b"member1", b"member2"] # "member1" and "member2" are random members of "my_sorted_set". + >>> await client.zrandmember("non_existing_sorted_set", 3) + [] # "non_existing_sorted_set" is not an existing key, so an empty list was returned. + """ + args: List[TEncodable] = [key, str(count)] + return cast( + List[bytes], + await self._execute_command(RequestType.ZRandMember, args), + ) + + async def zrandmember_withscores( + self, key: TEncodable, count: int + ) -> List[List[Union[bytes, float]]]: + """ + Retrieves up to the absolute value of `count` random members along with their scores from the sorted set + stored at 'key'. + + See https://valkey.io/commands/zrandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (int): The number of members to return. + If `count` is positive, returns unique members. + If `count` is negative, allows for duplicates members. + + Returns: + List[List[Union[bytes, float]]]: A list of `[member, score]` lists, where `member` is a random member from + the sorted set and `score` is the associated score. + If the sorted set does not exist or is empty, the response will be an empty list. + + Examples: + >>> await client.zadd("my_sorted_set", {"member1": 1.0, "member2": 2.0}) + >>> await client.zrandmember_withscores("my_sorted_set", -3) + [[b"member1", 1.0], [b"member1", 1.0], [b"member2", 2.0]] # "member1" and "member2" are random members of "my_sorted_set", and have scores of 1.0 and 2.0, respectively. + >>> await client.zrandmember_withscores("non_existing_sorted_set", 3) + [] # "non_existing_sorted_set" is not an existing key, so an empty list was returned. + """ + args: List[TEncodable] = [key, str(count), "WITHSCORES"] + return cast( + List[List[Union[bytes, float]]], + await self._execute_command(RequestType.ZRandMember, args), + ) + + async def zmpop( + self, + keys: List[TEncodable], + filter: ScoreFilter, + count: Optional[int] = None, + ) -> Optional[List[Union[bytes, Mapping[bytes, float]]]]: + """ + Pops a member-score pair from the first non-empty sorted set, with the given keys being checked in the order + that they are given. + + The optional `count` argument can be used to specify the number of elements to pop, and is + set to 1 by default. + + The number of popped elements is the minimum from the sorted set's cardinality and `count`. + + See https://valkey.io/commands/zmpop for more details. + + Note: + When in cluster mode, all `keys` must map to the same hash slot. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + filter (ScoreFilter): The element pop criteria - either ScoreFilter.MIN or ScoreFilter.MAX to pop + members with the lowest/highest scores accordingly. + count (Optional[int]): The number of elements to pop. + + Returns: + Optional[List[Union[bytes, Mapping[bytes, float]]]]: A two-element list containing the key name of the set from + which elements were popped, and a member-score mapping of the popped elements. If no members could be + popped, returns None. + + Examples: + >>> await client.zadd("zSet1", {"one": 1.0, "two": 2.0, "three": 3.0}) + >>> await client.zadd("zSet2", {"four": 4.0}) + >>> await client.zmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 2) + [b'zSet1', {b'three': 3.0, b'two': 2.0}] # "three" with score 3.0 and "two" with score 2.0 were popped from "zSet1". + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [str(len(keys))] + keys + [filter.value] + if count is not None: + args.extend(["COUNT", str(count)]) + + return cast( + Optional[List[Union[bytes, Mapping[bytes, float]]]], + await self._execute_command(RequestType.ZMPop, args), + ) + + async def bzmpop( + self, + keys: List[TEncodable], + modifier: ScoreFilter, + timeout: float, + count: Optional[int] = None, + ) -> Optional[List[Union[bytes, Mapping[bytes, float]]]]: + """ + Pops a member-score pair from the first non-empty sorted set, with the given keys being checked in the order + that they are given. Blocks the connection when there are no members to pop from any of the given sorted sets. + + The optional `count` argument can be used to specify the number of elements to pop, and is set to 1 by default. + + The number of popped elements is the minimum from the sorted set's cardinality and `count`. + + `BZMPOP` is the blocking variant of `ZMPOP`. + + See https://valkey.io/commands/bzmpop for more details. + + Notes: + 1. When in cluster mode, all `keys` must map to the same hash slot. + 2. `BZMPOP` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + modifier (ScoreFilter): The element pop criteria - either ScoreFilter.MIN or ScoreFilter.MAX to pop + members with the lowest/highest scores accordingly. + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of 0 will + block indefinitely. + count (Optional[int]): The number of elements to pop. + + Returns: + Optional[List[Union[bytes, Mapping[bytes, float]]]]: A two-element list containing the key name of the set from + which elements were popped, and a member-score mapping of the popped elements. If no members could be + popped and the timeout expired, returns None. + + Examples: + >>> await client.zadd("zSet1", {"one": 1.0, "two": 2.0, "three": 3.0}) + >>> await client.zadd("zSet2", {"four": 4.0}) + >>> await client.bzmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 0.5, 2) + [b'zSet1', {b'three': 3.0, b'two': 2.0}] # "three" with score 3.0 and "two" with score 2.0 were popped from "zSet1". + + Since: Valkey version 7.0.0. + """ + args = [str(timeout), str(len(keys))] + keys + [modifier.value] + if count is not None: + args = args + ["COUNT", str(count)] + + return cast( + Optional[List[Union[bytes, Mapping[bytes, float]]]], + await self._execute_command(RequestType.BZMPop, args), + ) + + async def zintercard( + self, keys: List[TEncodable], limit: Optional[int] = None + ) -> int: + """ + Returns the cardinality of the intersection of the sorted sets specified by `keys`. When provided with the + optional `limit` argument, if the intersection cardinality reaches `limit` partway through the computation, the + algorithm will exit early and yield `limit` as the cardinality. + + See https://valkey.io/commands/zintercard for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets to intersect. + limit (Optional[int]): An optional argument that can be used to specify a maximum number for the + intersection cardinality. If limit is not supplied, or if it is set to 0, there will be no limit. + + Note: + When in cluster mode, all `keys` must map to the same hash slot. + + Returns: + int: The cardinality of the intersection of the given sorted sets, or the `limit` if reached. + + Examples: + >>> await client.zadd("key1", {"member1": 10.5, "member2": 8.2, "member3": 9.6}) + >>> await client.zadd("key2", {"member1": 10.5, "member2": 3.5}) + >>> await client.zintercard(["key1", "key2"]) + 2 # Indicates that the intersection of the sorted sets at "key1" and "key2" has a cardinality of 2. + >>> await client.zintercard(["key1", "key2"], 1) + 1 # A `limit` of 1 was provided, so the intersection computation exits early and yields the `limit` value of 1. + + Since: Valkey version 7.0.0. + """ + args = [str(len(keys))] + keys + if limit is not None: + args.extend(["LIMIT", str(limit)]) + + return cast( + int, + await self._execute_command(RequestType.ZInterCard, args), + ) + + async def invoke_script( + self, + script: Script, + keys: Optional[List[TEncodable]] = None, + args: Optional[List[TEncodable]] = None, + ) -> TResult: + """ + Invokes a Lua script with its keys and arguments. + This method simplifies the process of invoking scripts on a the server by using an object that represents a Lua script. + The script loading, argument preparation, and execution will all be handled internally. + If the script has not already been loaded, it will be loaded automatically using the `SCRIPT LOAD` command. + After that, it will be invoked using the `EVALSHA` command. + + See https://valkey.io/commands/script-load/ and https://valkey.io/commands/evalsha/ for more details. + + Args: + script (Script): The Lua script to execute. + keys (Optional[List[TEncodable]]): The keys that are used in the script. + args (Optional[List[TEncodable]]): The arguments for the script. + + Returns: + TResult: a value that depends on the script that was executed. + + Examples: + >>> lua_script = Script("return { KEYS[1], ARGV[1] }") + >>> await invoke_script(lua_script, keys=["foo"], args=["bar"] ); + [b"foo", b"bar"] + """ + return await self._execute_script(script.get_hash(), keys, args) + + async def pfadd(self, key: TEncodable, elements: List[TEncodable]) -> int: + """ + Adds all elements to the HyperLogLog data structure stored at the specified `key`. + Creates a new structure if the `key` does not exist. + When no elements are provided, and `key` exists and is a HyperLogLog, then no operation is performed. + + See https://valkey.io/commands/pfadd/ for more details. + + Args: + key (TEncodable): The key of the HyperLogLog data structure to add elements into. + elements (List[TEncodable]): A list of members to add to the HyperLogLog stored at `key`. + + Returns: + int: If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is + altered, then returns 1. Otherwise, returns 0. + + Examples: + >>> await client.pfadd("hll_1", ["a", "b", "c" ]) + 1 # A data structure was created or modified + >>> await client.pfadd("hll_2", []) + 1 # A new empty data structure was created + """ + return cast( + int, + await self._execute_command(RequestType.PfAdd, [key] + elements), + ) + + async def pfcount(self, keys: List[TEncodable]) -> int: + """ + Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or + calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + + See https://valkey.io/commands/pfcount for more details. + + Note: + When in Cluster mode, all `keys` must map to the same hash slot. + + Args: + keys (List[TEncodable]): The keys of the HyperLogLog data structures to be analyzed. + + Returns: + int: The approximated cardinality of given HyperLogLog data structures. + The cardinality of a key that does not exist is 0. + + Examples: + >>> await client.pfcount(["hll_1", "hll_2"]) + 4 # The approximated cardinality of the union of "hll_1" and "hll_2" is 4. + """ + return cast( + int, + await self._execute_command(RequestType.PfCount, keys), + ) + + async def pfmerge( + self, destination: TEncodable, source_keys: List[TEncodable] + ) -> TOK: + """ + Merges multiple HyperLogLog values into a unique value. If the destination variable exists, it is treated as one + of the source HyperLogLog data sets, otherwise a new HyperLogLog is created. + + See https://valkey.io/commands/pfmerge for more details. + + Note: + When in Cluster mode, all keys in `source_keys` and `destination` must map to the same hash slot. + + Args: + destination (TEncodable): The key of the destination HyperLogLog where the merged data sets will be stored. + source_keys (List[TEncodable]): The keys of the HyperLogLog structures to be merged. + + Returns: + OK: A simple OK response. + + Examples: + >>> await client.pfadd("hll1", ["a", "b"]) + >>> await client.pfadd("hll2", ["b", "c"]) + >>> await client.pfmerge("new_hll", ["hll1", "hll2"]) + OK # The value of "hll1" merged with "hll2" was stored in "new_hll". + >>> await client.pfcount(["new_hll"]) + 3 # The approximated cardinality of "new_hll" is 3. + """ + return cast( + TOK, + await self._execute_command( + RequestType.PfMerge, [destination] + source_keys + ), + ) + + async def bitcount( + self, key: TEncodable, options: Optional[OffsetOptions] = None + ) -> int: + """ + Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can + optionally be provided to count the number of bits in a specific string interval. + + See https://valkey.io/commands/bitcount for more details. + + Args: + key (TEncodable): The key for the string to count the set bits of. + options (Optional[OffsetOptions]): The offset options. + + Returns: + int: If `options` is provided, returns the number of set bits in the string interval specified by `options`. + If `options` is not provided, returns the number of set bits in the string stored at `key`. + Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. + + Examples: + >>> await client.bitcount("my_key1") + 2 # The string stored at "my_key1" contains 2 set bits. + >>> await client.bitcount("my_key2", OffsetOptions(1, 3)) + 2 # The second to fourth bytes of the string stored at "my_key2" contain 2 set bits. + >>> await client.bitcount("my_key3", OffsetOptions(1, 1, BitmapIndexType.BIT)) + 1 # Indicates that the second bit of the string stored at "my_key3" is set. + >>> await client.bitcount("my_key3", OffsetOptions(-1, -1, BitmapIndexType.BIT)) + 1 # Indicates that the last bit of the string stored at "my_key3" is set. + """ + args: List[TEncodable] = [key] + if options is not None: + args.extend(options.to_args()) + + return cast( + int, + await self._execute_command(RequestType.BitCount, args), + ) + + async def setbit(self, key: TEncodable, offset: int, value: int) -> int: + """ + Sets or clears the bit at `offset` in the string value stored at `key`. The `offset` is a zero-based index, + with `0` being the first element of the list, `1` being the next element, and so on. The `offset` must be less + than `2^32` and greater than or equal to `0`. If a key is non-existent then the bit at `offset` is set to + `value` and the preceding bits are set to `0`. + + See https://valkey.io/commands/setbit for more details. + + Args: + key (TEncodable): The key of the string. + offset (int): The index of the bit to be set. + value (int): The bit value to set at `offset`. The value must be `0` or `1`. + + Returns: + int: The bit value that was previously stored at `offset`. + + Examples: + >>> await client.setbit("string_key", 1, 1) + 0 # The second bit value was 0 before setting to 1. + """ + return cast( + int, + await self._execute_command( + RequestType.SetBit, [key, str(offset), str(value)] + ), + ) + + async def getbit(self, key: TEncodable, offset: int) -> int: + """ + Returns the bit value at `offset` in the string value stored at `key`. + `offset` should be greater than or equal to zero. + + See https://valkey.io/commands/getbit for more details. + + Args: + key (TEncodable): The key of the string. + offset (int): The index of the bit to return. + + Returns: + int: The bit at the given `offset` of the string. Returns `0` if the key is empty or if the `offset` exceeds + the length of the string. + + Examples: + >>> await client.getbit("my_key", 1) + 1 # Indicates that the second bit of the string stored at "my_key" is set to 1. + """ + return cast( + int, + await self._execute_command(RequestType.GetBit, [key, str(offset)]), + ) + + async def bitpos( + self, key: TEncodable, bit: int, start: Optional[int] = None + ) -> int: + """ + Returns the position of the first bit matching the given `bit` value. The optional starting offset + `start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on. + The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being + the last byte of the list, `-2` being the penultimate, and so on. + + See https://valkey.io/commands/bitpos for more details. + + Args: + key (TEncodable): The key of the string. + bit (int): The bit value to match. Must be `0` or `1`. + start (Optional[int]): The starting offset. + + Returns: + int: The position of the first occurrence of `bit` in the binary value of the string held at `key`. + If `start` was provided, the search begins at the offset indicated by `start`. + + Examples: + >>> await client.set("key1", "A1") # "A1" has binary value 01000001 00110001 + >>> await client.bitpos("key1", 1) + 1 # The first occurrence of bit value 1 in the string stored at "key1" is at the second position. + >>> await client.bitpos("key1", 1, -1) + 10 # The first occurrence of bit value 1, starting at the last byte in the string stored at "key1", is at the eleventh position. + """ + args = [key, str(bit)] if start is None else [key, str(bit), str(start)] + return cast( + int, + await self._execute_command(RequestType.BitPos, args), + ) + + async def bitpos_interval( + self, + key: TEncodable, + bit: int, + start: int, + end: int, + index_type: Optional[BitmapIndexType] = None, + ) -> int: + """ + Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with + `0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative + numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2` + being the penultimate, and so on. + + If you are using Valkey 7.0.0 or above, the optional `index_type` can also be provided to specify whether the + `start` and `end` offsets specify BIT or BYTE offsets. If `index_type` is not provided, BYTE offsets + are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is + specified, `start=0` and `end=2` means to look at the first three bytes. + + See https://valkey.io/commands/bitpos for more details. + + Args: + key (TEncodable): The key of the string. + bit (int): The bit value to match. Must be `0` or `1`. + start (int): The starting offset. + end (int): The ending offset. + index_type (Optional[BitmapIndexType]): The index offset type. This option can only be specified if you are + using Valkey version 7.0.0 or above. Could be either `BitmapIndexType.BYTE` or `BitmapIndexType.BIT`. + If no index type is provided, the indexes will be assumed to be byte indexes. + + Returns: + int: The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the binary + value of the string held at `key`. + + Examples: + >>> await client.set("key1", "A12") # "A12" has binary value 01000001 00110001 00110010 + >>> await client.bitpos_interval("key1", 1, 1, -1) + 10 # The first occurrence of bit value 1 in the second byte to the last byte of the string stored at "key1" is at the eleventh position. + >>> await client.bitpos_interval("key1", 1, 2, 9, BitmapIndexType.BIT) + 7 # The first occurrence of bit value 1 in the third to tenth bits of the string stored at "key1" is at the eighth position. + """ + if index_type is not None: + args = [key, str(bit), str(start), str(end), index_type.value] + else: + args = [key, str(bit), str(start), str(end)] + + return cast( + int, + await self._execute_command(RequestType.BitPos, args), + ) + + async def bitop( + self, + operation: BitwiseOperation, + destination: TEncodable, + keys: List[TEncodable], + ) -> int: + """ + Perform a bitwise operation between multiple keys (containing string values) and store the result in the + `destination`. + + See https://valkey.io/commands/bitop for more details. + + Note: + When in cluster mode, `destination` and all `keys` must map to the same hash slot. + + Args: + operation (BitwiseOperation): The bitwise operation to perform. + destination (TEncodable): The key that will store the resulting string. + keys (List[TEncodable]): The list of keys to perform the bitwise operation on. + + Returns: + int: The size of the string stored in `destination`. + + Examples: + >>> await client.set("key1", "A") # "A" has binary value 01000001 + >>> await client.set("key1", "B") # "B" has binary value 01000010 + >>> await client.bitop(BitwiseOperation.AND, "destination", ["key1", "key2"]) + 1 # The size of the resulting string stored in "destination" is 1 + >>> await client.get("destination") + "@" # "@" has binary value 01000000 + """ + return cast( + int, + await self._execute_command( + RequestType.BitOp, [operation.value, destination] + keys + ), + ) + + async def bitfield( + self, key: TEncodable, subcommands: List[BitFieldSubCommands] + ) -> List[Optional[int]]: + """ + Reads or modifies the array of bits representing the string that is held at `key` based on the specified + `subcommands`. + + See https://valkey.io/commands/bitfield for more details. + + Args: + key (TEncodable): The key of the string. + subcommands (List[BitFieldSubCommands]): The subcommands to be performed on the binary value of the string + at `key`, which could be any of the following: + - `BitFieldGet` + - `BitFieldSet` + - `BitFieldIncrBy` + - `BitFieldOverflow` + + Returns: + List[Optional[int]]: An array of results from the executed subcommands: + - `BitFieldGet` returns the value in `Offset` or `OffsetMultiplier`. + - `BitFieldSet` returns the old value in `Offset` or `OffsetMultiplier`. + - `BitFieldIncrBy` returns the new value in `Offset` or `OffsetMultiplier`. + - `BitFieldOverflow` determines the behavior of the "SET" and "INCRBY" subcommands when an overflow or + underflow occurs. "OVERFLOW" does not return a value and does not contribute a value to the list + response. + + Examples: + >>> await client.set("my_key", "A") # "A" has binary value 01000001 + >>> await client.bitfield("my_key", [BitFieldSet(UnsignedEncoding(2), Offset(1), 3), BitFieldGet(UnsignedEncoding(2), Offset(1))]) + [2, 3] # The old value at offset 1 with an unsigned encoding of 2 was 2. The new value at offset 1 with an unsigned encoding of 2 is 3. + """ + args = [key] + _create_bitfield_args(subcommands) + return cast( + List[Optional[int]], + await self._execute_command(RequestType.BitField, args), + ) + + async def bitfield_read_only( + self, key: TEncodable, subcommands: List[BitFieldGet] + ) -> List[int]: + """ + Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. + + See https://valkey.io/commands/bitfield_ro for more details. + + Args: + key (TEncodable): The key of the string. + subcommands (List[BitFieldGet]): The "GET" subcommands to be performed. + + Returns: + List[int]: An array of results from the "GET" subcommands. + + Examples: + >>> await client.set("my_key", "A") # "A" has binary value 01000001 + >>> await client.bitfield_read_only("my_key", [BitFieldGet(UnsignedEncoding(2), Offset(1))]) + [2] # The value at offset 1 with an unsigned encoding of 2 is 3. + + Since: Valkey version 6.0.0. + """ + args = [key] + _create_bitfield_read_only_args(subcommands) + return cast( + List[int], + await self._execute_command(RequestType.BitFieldReadOnly, args), + ) + + async def object_encoding(self, key: TEncodable) -> Optional[bytes]: + """ + Returns the internal encoding for the Valkey object stored at `key`. + + See https://valkey.io/commands/object-encoding for more details. + + Args: + key (TEncodable): The `key` of the object to get the internal encoding of. + + Returns: + Optional[bytes]: If `key` exists, returns the internal encoding of the object stored at + `key` as a bytes string. Otherwise, returns None. + + Examples: + >>> await client.object_encoding("my_hash") + b"listpack" # The hash stored at "my_hash" has an internal encoding of "listpack". + """ + return cast( + Optional[bytes], + await self._execute_command(RequestType.ObjectEncoding, [key]), + ) + + async def object_freq(self, key: TEncodable) -> Optional[int]: + """ + Returns the logarithmic access frequency counter of a Valkey object stored at `key`. + + See https://valkey.io/commands/object-freq for more details. + + Args: + key (TEncodable): The key of the object to get the logarithmic access frequency counter of. + + Returns: + Optional[int]: If `key` exists, returns the logarithmic access frequency counter of the object stored at `key` as an + integer. Otherwise, returns None. + + Examples: + >>> await client.object_freq("my_hash") + 2 # The logarithmic access frequency counter of "my_hash" has a value of 2. + """ + return cast( + Optional[int], + await self._execute_command(RequestType.ObjectFreq, [key]), + ) + + async def object_idletime(self, key: TEncodable) -> Optional[int]: + """ + Returns the time in seconds since the last access to the value stored at `key`. + + See https://valkey.io/commands/object-idletime for more details. + + Args: + key (TEncodable): The key of the object to get the idle time of. + + Returns: + Optional[int]: If `key` exists, returns the idle time in seconds. Otherwise, returns None. + + Examples: + >>> await client.object_idletime("my_hash") + 13 # "my_hash" was last accessed 13 seconds ago. + """ + return cast( + Optional[int], + await self._execute_command(RequestType.ObjectIdleTime, [key]), + ) + + async def object_refcount(self, key: TEncodable) -> Optional[int]: + """ + Returns the reference count of the object stored at `key`. + + See https://valkey.io/commands/object-refcount for more details. + + Args: + key (TEncodable): The key of the object to get the reference count of. + + Returns: + Optional[int]: If `key` exists, returns the reference count of the object stored at `key` as an integer. + Otherwise, returns None. + + Examples: + >>> await client.object_refcount("my_hash") + 2 # "my_hash" has a reference count of 2. + """ + return cast( + Optional[int], + await self._execute_command(RequestType.ObjectRefCount, [key]), + ) + + async def srandmember(self, key: TEncodable) -> Optional[bytes]: + """ + Returns a random element from the set value stored at 'key'. + + See https://valkey.io/commands/srandmember for more details. + + Args: + key (TEncodable): The key from which to retrieve the set member. + + Returns: + Optional[bytes]: A random element from the set, or None if 'key' does not exist. + + Examples: + >>> await client.sadd("my_set", {"member1": 1.0, "member2": 2.0}) + >>> await client.srandmember(b"my_set") + b"member1" # "member1" is a random member of "my_set". + >>> await client.srandmember("non_existing_set") + None # "non_existing_set" is not an existing key, so None was returned. + """ + args: List[TEncodable] = [key] + return cast( + Optional[bytes], + await self._execute_command(RequestType.SRandMember, args), + ) + + async def srandmember_count(self, key: TEncodable, count: int) -> List[bytes]: + """ + Returns one or more random elements from the set value stored at 'key'. + + See https://valkey.io/commands/srandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (int): The number of members to return. + If `count` is positive, returns unique members. + If `count` is negative, allows for duplicates members. + + Returns: + List[bytes]: A list of members from the set. + If the set does not exist or is empty, the response will be an empty list. + + Examples: + >>> await client.sadd("my_set", {"member1": 1.0, "member2": 2.0}) + >>> await client.srandmember("my_set", -3) + [b"member1", b"member1", b"member2"] # "member1" and "member2" are random members of "my_set". + >>> await client.srandmember("non_existing_set", 3) + [] # "non_existing_set" is not an existing key, so an empty list was returned. + """ + return cast( + List[bytes], + await self._execute_command(RequestType.SRandMember, [key, str(count)]), + ) + + async def getex( + self, + key: TEncodable, + expiry: Optional[ExpiryGetEx] = None, + ) -> Optional[bytes]: + """ + Get the value of `key` and optionally set its expiration. `GETEX` is similar to `GET`. + See https://valkey.io/commands/getex for more details. + + Args: + key (TEncodable): The key to get. + expiry (Optional[ExpiryGetEx], optional): set expiriation to the given key. + Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `PERSIST`] in the Valkey API. + + Returns: + Optional[bytes]: + If `key` exists, return the value stored at `key` + If `key` does not exist, return `None` + + Examples: + >>> await client.set("key", "value") + 'OK' + >>> await client.getex("key") + b'value' + >>> await client.getex("key", ExpiryGetEx(ExpiryTypeGetEx.SEC, 1)) + b'value' + >>> time.sleep(1) + >>> await client.getex(b"key") + None + + Since: Valkey version 6.2.0. + """ + args = [key] + if expiry is not None: + args.extend(expiry.get_cmd_args()) + return cast( + Optional[bytes], + await self._execute_command(RequestType.GetEx, args), + ) + + async def dump( + self, + key: TEncodable, + ) -> Optional[bytes]: + """ + Serialize the value stored at `key` in a Valkey-specific format and return it to the user. + + See https://valkey.io/commands/dump for more details. + + Args: + key (TEncodable): The `key` to serialize. + + Returns: + Optional[bytes]: The serialized value of the data stored at `key`. + If `key` does not exist, `None` will be returned. + + Examples: + >>> await client.dump("key") + b"value" # The serialized value stored at `key`. + >>> await client.dump("nonExistingKey") + None # Non-existing key will return `None`. + """ + return cast( + Optional[bytes], + await self._execute_command(RequestType.Dump, [key]), + ) + + async def restore( + self, + key: TEncodable, + ttl: int, + value: TEncodable, + replace: bool = False, + absttl: bool = False, + idletime: Optional[int] = None, + frequency: Optional[int] = None, + ) -> TOK: + """ + Create a `key` associated with a `value` that is obtained by deserializing the provided + serialized `value` obtained via `dump`. + + See https://valkey.io/commands/restore for more details. + + Args: + key (TEncodable): The `key` to create. + ttl (int): The expiry time (in milliseconds). If `0`, the `key` will persist. + value (TEncodable) The serialized value to deserialize and assign to `key`. + replace (bool): Set to `True` to replace the key if it exists. + absttl (bool): Set to `True` to specify that `ttl` represents an absolute Unix + timestamp (in milliseconds). + idletime (Optional[int]): Set the `IDLETIME` option with object idletime to the given key. + frequency (Optional[int]): Set the `FREQ` option with object frequency to the given key. + + Returns: + OK: If the `key` was successfully restored with a `value`. + + Examples: + >>> await client.restore("newKey", 0, value) + OK # Indicates restore `newKey` without any ttl expiry nor any option + >>> await client.restore("newKey", 0, value, replace=True) + OK # Indicates restore `newKey` with `REPLACE` option + >>> await client.restore("newKey", 0, value, absttl=True) + OK # Indicates restore `newKey` with `ABSTTL` option + >>> await client.restore("newKey", 0, value, idletime=10) + OK # Indicates restore `newKey` with `IDLETIME` option + >>> await client.restore("newKey", 0, value, frequency=5) + OK # Indicates restore `newKey` with `FREQ` option + """ + args = [key, str(ttl), value] + if replace is True: + args.append("REPLACE") + if absttl is True: + args.append("ABSTTL") + if idletime is not None: + args.extend(["IDLETIME", str(idletime)]) + if frequency is not None: + args.extend(["FREQ", str(frequency)]) + return cast( + TOK, + await self._execute_command(RequestType.Restore, args), + ) + + async def sscan( + self, + key: TEncodable, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + ) -> List[Union[bytes, List[bytes]]]: + """ + Iterates incrementally over a set. + + See https://valkey.io/commands/sscan for more details. + + Args: + key (TEncodable): The key of the set. + cursor (TEncodable): The cursor that points to the next iteration of results. A value of "0" indicates the start of + the search. + match (Optional[TEncodable]): The match filter is applied to the result of the command and will only include + strings or byte strings that match the pattern specified. If the set is large enough for scan commands to return only a + subset of the set then there could be a case where the result is empty although there are items that + match the pattern specified. This is due to the default `COUNT` being `10` which indicates that it will + only fetch and match `10` items from the list. + count (Optional[int]): `COUNT` is a just a hint for the command for how many elements to fetch from the set. + `COUNT` could be ignored until the set is large enough for the `SCAN` commands to represent the results + as compact single-allocation packed encoding. + + Returns: + List[Union[bytes, List[bytes]]]: An `Array` of the `cursor` and the subset of the set held by `key`. + The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + returned on the last iteration of the set. The second element is always an `Array` of the subset of the + set held in `key`. + + Examples: + # Assume "key" contains a set with 130 members + >>> result_cursor = "0" + >>> while True: + ... result = await client.sscan("key", "0", match="*") + ... new_cursor = str(result [0]) + ... print("Cursor: ", new_cursor) + ... print("Members: ", result[1]) + ... if new_cursor == "0": + ... break + ... result_cursor = new_cursor + Cursor: 48 + Members: [b'3', b'118', b'120', b'86', b'76', b'13', b'61', b'111', b'55', b'45'] + Cursor: 24 + Members: [b'38', b'109', b'11', b'119', b'34', b'24', b'40', b'57', b'20', b'17'] + Cursor: 0 + Members: [b'47', b'122', b'1', b'53', b'10', b'14', b'80'] + """ + args: List[TEncodable] = [key, cursor] + if match is not None: + args += ["MATCH", match] + if count is not None: + args += ["COUNT", str(count)] + + return cast( + List[Union[bytes, List[bytes]]], + await self._execute_command(RequestType.SScan, args), + ) + + async def zscan( + self, + key: TEncodable, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + ) -> List[Union[bytes, List[bytes]]]: + """ + Iterates incrementally over a sorted set. + + See https://valkey.io/commands/zscan for more details. + + Args: + key (TEncodable): The key of the sorted set. + cursor (TEncodable): The cursor that points to the next iteration of results. A value of "0" indicates the start of + the search. + match (Optional[TEncodable]): The match filter is applied to the result of the command and will only include + strings or byte strings that match the pattern specified. If the sorted set is large enough for scan commands to return + only a subset of the sorted set then there could be a case where the result is empty although there are + items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates + that it will only fetch and match `10` items from the list. + count (Optional[int]): `COUNT` is a just a hint for the command for how many elements to fetch from the + sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to + represent the results as compact single-allocation packed encoding. + + Returns: + List[Union[bytes, List[bytes]]]: An `Array` of the `cursor` and the subset of the sorted set held by `key`. + The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + returned on the last iteration of the sorted set. The second element is always an `Array` of the subset + of the sorted set held in `key`. The `Array` in the second element is always a flattened series of + `String` pairs, where the value is at even indices and the score is at odd indices. + + Examples: + # Assume "key" contains a sorted set with multiple members + >>> result_cursor = "0" + >>> while True: + ... result = await client.zscan("key", "0", match="*", count=5) + ... new_cursor = str(result [0]) + ... print("Cursor: ", new_cursor) + ... print("Members: ", result[1]) + ... if new_cursor == "0": + ... break + ... result_cursor = new_cursor + Cursor: 123 + Members: [b'value 163', b'163', b'value 114', b'114', b'value 25', b'25', b'value 82', b'82', b'value 64', b'64'] + Cursor: 47 + Members: [b'value 39', b'39', b'value 127', b'127', b'value 43', b'43', b'value 139', b'139', b'value 211', b'211'] + Cursor: 0 + Members: [b'value 55', b'55', b'value 24', b'24', b'value 90', b'90', b'value 113', b'113'] + """ + args: List[TEncodable] = [key, cursor] + if match is not None: + args += ["MATCH", match] + if count is not None: + args += ["COUNT", str(count)] + + return cast( + List[Union[bytes, List[bytes]]], + await self._execute_command(RequestType.ZScan, args), + ) + + async def hscan( + self, + key: TEncodable, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + ) -> List[Union[bytes, List[bytes]]]: + """ + Iterates incrementally over a hash. + + See https://valkey.io/commands/hscan for more details. + + Args: + key (TEncodable): The key of the set. + cursor (TEncodable): The cursor that points to the next iteration of results. A value of "0" indicates the start of + the search. + match (Optional[TEncodable]): The match filter is applied to the result of the command and will only include + strings or byte strings that match the pattern specified. If the hash is large enough for scan commands to return only a + subset of the hash then there could be a case where the result is empty although there are items that + match the pattern specified. This is due to the default `COUNT` being `10` which indicates that it will + only fetch and match `10` items from the list. + count (Optional[int]): `COUNT` is a just a hint for the command for how many elements to fetch from the hash. + `COUNT` could be ignored until the hash is large enough for the `SCAN` commands to represent the results + as compact single-allocation packed encoding. + + Returns: + List[Union[bytes, List[bytes]]]: An `Array` of the `cursor` and the subset of the hash held by `key`. + The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + returned on the last iteration of the hash. The second element is always an `Array` of the subset of the + hash held in `key`. The `Array` in the second element is always a flattened series of `String` pairs, + where the value is at even indices and the score is at odd indices. + + Examples: + # Assume "key" contains a hash with multiple members + >>> result_cursor = "0" + >>> while True: + ... result = await client.hscan("key", "0", match="*", count=3) + ... new_cursor = str(result [0]) + ... print("Cursor: ", new_cursor) + ... print("Members: ", result[1]) + ... if new_cursor == "0": + ... break + ... result_cursor = new_cursor + Cursor: 1 + Members: [b'field 79', b'value 79', b'field 20', b'value 20', b'field 115', b'value 115'] + Cursor: 39 + Members: [b'field 63', b'value 63', b'field 293', b'value 293', b'field 162', b'value 162'] + Cursor: 0 + Members: [b'field 420', b'value 420', b'field 221', b'value 221'] + """ + args: List[TEncodable] = [key, cursor] + if match is not None: + args += ["MATCH", match] + if count is not None: + args += ["COUNT", str(count)] + + return cast( + List[Union[bytes, List[bytes]]], + await self._execute_command(RequestType.HScan, args), + ) + + async def fcall( + self, + function: TEncodable, + keys: Optional[List[TEncodable]] = None, + arguments: Optional[List[TEncodable]] = None, + ) -> TResult: + """ + Invokes a previously loaded function. + See https://valkey.io/commands/fcall/ for more details. + When in cluster mode, all keys in `keys` must map to the same hash slot. + Args: + function (TEncodable): The function name. + keys (Optional[List[TEncodable]]): A list of keys accessed by the function. To ensure the correct + execution of functions, both in standalone and clustered deployments, all names of keys + that a function accesses must be explicitly provided as `keys`. + arguments (Optional[List[TEncodable]]): A list of `function` arguments. `Arguments` + should not represent names of keys. + Returns: + TResult: + The invoked function's return value. + Example: + >>> await client.fcall("Deep_Thought") + b'new_value' # Returns the function's return value. + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [] + if keys is not None: + args.extend([function, str(len(keys))] + keys) + else: + args.extend([function, str(0)]) + if arguments is not None: + args.extend(arguments) + return cast( + TResult, + await self._execute_command(RequestType.FCall, args), + ) + + async def fcall_ro( + self, + function: TEncodable, + keys: Optional[List[TEncodable]] = None, + arguments: Optional[List[TEncodable]] = None, + ) -> TResult: + """ + Invokes a previously loaded read-only function. + + See https://valkey.io/commands/fcall_ro for more details. + + When in cluster mode, all keys in `keys` must map to the same hash slot. + + Args: + function (TEncodable): The function name. + keys (List[TEncodable]): An `array` of keys accessed by the function. To ensure the correct + execution of functions, all names of keys that a function accesses must be + explicitly provided as `keys`. + arguments (List[TEncodable]): An `array` of `function` arguments. `arguments` should not + represent names of keys. + + Returns: + TResult: The return value depends on the function that was executed. + + Examples: + >>> await client.fcall_ro("Deep_Thought", ["key1"], ["Answer", "to", "the", + "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"]) + 42 # The return value on the function that was executed + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [] + if keys is not None: + args.extend([function, str(len(keys))] + keys) + else: + args.extend([function, str(0)]) + if arguments is not None: + args.extend(arguments) + return cast( + TResult, + await self._execute_command(RequestType.FCallReadOnly, args), + ) + + async def watch(self, keys: List[TEncodable]) -> TOK: + """ + Marks the given keys to be watched for conditional execution of a transaction. Transactions + will only execute commands if the watched keys are not modified before execution of the + transaction. + + See https://valkey.io/commands/watch for more details. + + Note: + When in cluster mode, the command may route to multiple nodes when `keys` map to different hash slots. + + Args: + keys (List[TEncodable]): The keys to watch. + + Returns: + TOK: A simple "OK" response. + + Examples: + >>> await client.watch("sampleKey") + 'OK' + >>> transaction.set("sampleKey", "foobar") + >>> await client.exec(transaction) + 'OK' # Executes successfully and keys are unwatched. + + >>> await client.watch("sampleKey") + 'OK' + >>> transaction.set("sampleKey", "foobar") + >>> await client.set("sampleKey", "hello world") + 'OK' + >>> await client.exec(transaction) + None # None is returned when the watched key is modified before transaction execution. + """ + + return cast( + TOK, + await self._execute_command(RequestType.Watch, keys), + ) + + @dataclass + class PubSubMsg: + """ + Describes the incoming pubsub message + + Attributes: + message (TEncodable): Incoming message. + channel (TEncodable): Name of an channel that triggered the message. + pattern (Optional[TEncodable]): Pattern that triggered the message. + """ + + message: TEncodable + channel: TEncodable + pattern: Optional[TEncodable] + + async def get_pubsub_message(self) -> PubSubMsg: + """ + Returns the next pubsub message. + Throws WrongConfiguration in cases: + 1. No pubsub subscriptions are configured for the client + 2. Callback is configured with the pubsub subsciptions + + See https://valkey.io/docs/topics/pubsub/ for more details. + + Returns: + PubSubMsg: The next pubsub message + + Examples: + >>> pubsub_msg = await listening_client.get_pubsub_message() + """ + ... + + def try_get_pubsub_message(self) -> Optional[PubSubMsg]: + """ + Tries to return the next pubsub message. + Throws WrongConfiguration in cases: + 1. No pubsub subscriptions are configured for the client + 2. Callback is configured with the pubsub subsciptions + + See https://valkey.io/docs/topics/pubsub/ for more details. + + Returns: + Optional[PubSubMsg]: The next pubsub message or None + + Examples: + >>> pubsub_msg = listening_client.try_get_pubsub_message() + """ + ... + + async def lcs( + self, + key1: TEncodable, + key2: TEncodable, + ) -> bytes: + """ + Returns the longest common subsequence between strings stored at key1 and key2. + + Note that this is different than the longest common string algorithm, since + matching characters in the two strings do not need to be contiguous. + + For instance the LCS between "foo" and "fao" is "fo", since scanning the two strings + from left to right, the longest common set of characters is composed of the first "f" and then the "o". + + See https://valkey.io/commands/lcs for more details. + + Args: + key1 (TEncodable): The key that stores the first string. + key2 (TEncodable): The key that stores the second string. + + Returns: + A Bytes String containing the longest common subsequence between the 2 strings. + An empty String is returned if the keys do not exist or have no common subsequences. + + Examples: + >>> await client.mset({"testKey1" : "abcd", "testKey2": "axcd"}) + b'OK' + >>> await client.lcs("testKey1", "testKey2") + b'acd' + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [key1, key2] + + return cast( + bytes, + await self._execute_command(RequestType.LCS, args), + ) + + async def lcs_len( + self, + key1: TEncodable, + key2: TEncodable, + ) -> int: + """ + Returns the length of the longest common subsequence between strings stored at key1 and key2. + + Note that this is different than the longest common string algorithm, since + matching characters in the two strings do not need to be contiguous. + + For instance the LCS between "foo" and "fao" is "fo", since scanning the two strings + from left to right, the longest common set of characters is composed of the first "f" and then the "o". + + See https://valkey.io/commands/lcs for more details. + + Args: + key1 (TEncodable): The key that stores the first string value. + key2 (TEncodable): The key that stores the second string value. + + Returns: + The length of the longest common subsequence between the 2 strings. + + Examples: + >>> await client.mset({"testKey1" : "abcd", "testKey2": "axcd"}) + 'OK' + >>> await client.lcs_len("testKey1", "testKey2") + 3 # the length of the longest common subsequence between these 2 strings (b"acd") is 3. + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [key1, key2, "LEN"] + + return cast( + int, + await self._execute_command(RequestType.LCS, args), + ) + + async def lcs_idx( + self, + key1: TEncodable, + key2: TEncodable, + min_match_len: Optional[int] = None, + with_match_len: Optional[bool] = False, + ) -> Mapping[bytes, Union[List[List[Union[List[int], int]]], int]]: + """ + Returns the indices and length of the longest common subsequence between strings stored at key1 and key2. + + Note that this is different than the longest common string algorithm, since + matching characters in the two strings do not need to be contiguous. + + For instance the LCS between "foo" and "fao" is "fo", since scanning the two strings + from left to right, the longest common set of characters is composed of the first "f" and then the "o". + + See https://valkey.io/commands/lcs for more details. + + Args: + key1 (TEncodable): The key that stores the first string value. + key2 (TEncodable): The key that stores the second string value. + min_match_len (Optional[int]): The minimum length of matches to include in the result. + with_match_len (Optional[bool]): If True, include the length of the substring matched for each substring. + + Returns: + A Mapping containing the indices of the longest common subsequence between the + 2 strings and the length of the longest common subsequence. The resulting map contains two + keys, "matches" and "len": + - "len" is mapped to the length of the longest common subsequence between the 2 strings. + - "matches" is mapped to a three dimensional int array that stores pairs of indices that + represent the location of the common subsequences in the strings held by key1 and key2, + with the length of the match after each matches, if with_match_len is enabled. + + Examples: + >>> await client.mset({"testKey1" : "abcd1234", "testKey2": "bcdef1234"}) + 'OK' + >>> await client.lcs_idx("testKey1", "testKey2") + { + b'matches': [ + [ + [4, 7], # starting and ending indices of the subsequence b"1234" in b"abcd1234" (testKey1) + [5, 8], # starting and ending indices of the subsequence b"1234" in b"bcdef1234" (testKey2) + ], + [ + [1, 3], # starting and ending indices of the subsequence b"bcd" in b"abcd1234" (testKey1) + [0, 2], # starting and ending indices of the subsequence b"bcd" in b"bcdef1234" (testKey2) + ], + ], + b'len': 7 # length of the entire longest common subsequence + } + >>> await client.lcs_idx("testKey1", "testKey2", min_match_len=4) + { + b'matches': [ + [ + [4, 7], + [5, 8], + ], + # the other match with a length of 3 is excluded + ], + b'len': 7 + } + >>> await client.lcs_idx("testKey1", "testKey2", with_match_len=True) + { + b'matches': [ + [ + [4, 7], + [5, 8], + 4, # length of this match (b"1234") + ], + [ + [1, 3], + [0, 2], + 3, # length of this match (b"bcd") + ], + ], + b'len': 7 + } + + Since: Valkey version 7.0.0. + """ + args: List[TEncodable] = [key1, key2, "IDX"] + + if min_match_len is not None: + args.extend(["MINMATCHLEN", str(min_match_len)]) + + if with_match_len: + args.append("WITHMATCHLEN") + + return cast( + Mapping[bytes, Union[List[List[Union[List[int], int]]], int]], + await self._execute_command(RequestType.LCS, args), + ) + + async def lpos( + self, + key: TEncodable, + element: TEncodable, + rank: Optional[int] = None, + count: Optional[int] = None, + max_len: Optional[int] = None, + ) -> Union[int, List[int], None]: + """ + Returns the index or indexes of element(s) matching `element` in the `key` list. If no match is found, + None is returned. + + See https://valkey.io/commands/lpos for more details. + + Args: + key (TEncodable): The name of the list. + element (TEncodable): The value to search for within the list. + rank (Optional[int]): The rank of the match to return. + count (Optional[int]): The number of matches wanted. A `count` of 0 returns all the matches. + max_len (Optional[int]): The maximum number of comparisons to make between the element and the items + in the list. A `max_len` of 0 means unlimited amount of comparisons. + + Returns: + Union[int, List[int], None]: The index of the first occurrence of `element`, + or None if `element` is not in the list. + With the `count` option, a list of indices of matching elements will be returned. + + Examples: + >>> await client.rpush(key, ['a', 'b', 'c', '1', '2', '3', 'c', 'c']) + >>> await client.lpos(key, 'c') + 2 + >>> await client.lpos(key, 'c', rank = 2) + 6 + >>> await client.lpos(key, 'c', rank = -1) + 7 + >>> await client.lpos(key, 'c', count = 2) + [2, 6] + >>> await client.lpos(key, 'c', count = 0) + [2, 6, 7] + + Since: Valkey version 6.0.6. + """ + args: List[TEncodable] = [key, element] + + if rank is not None: + args.extend(["RANK", str(rank)]) + + if count is not None: + args.extend(["COUNT", str(count)]) + + if max_len is not None: + args.extend(["MAXLEN", str(max_len)]) + + return cast( + Union[int, List[int], None], + await self._execute_command(RequestType.LPos, args), + ) diff --git a/python/python/glide/async_commands/redis_modules/json.py b/python/python/glide/async_commands/server_modules/json.py similarity index 71% rename from python/python/glide/async_commands/redis_modules/json.py rename to python/python/glide/async_commands/server_modules/json.py index 3bcaeedb97..d75c2c7a4b 100644 --- a/python/python/glide/async_commands/redis_modules/json.py +++ b/python/python/glide/async_commands/server_modules/json.py @@ -1,4 +1,4 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 """module for `RedisJSON` commands. Examples: @@ -11,21 +11,21 @@ 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. >>> json_get = await redisJson.get(client, "doc", "$") # Returns the value at path '$' in the JSON document stored at `doc` as JSON string. >>> print(json_get) - "[{\"a\":1.0,\"b\":2}]" - >>> json.loads(json_get) + b"[{\"a\":1.0,\"b\":2}]" + >>> json.loads(str(json_get)) [{"a": 1.0, "b" :2}] # JSON object retrieved from the key `doc` using json.loads() """ from typing import List, Optional, Union, cast from glide.async_commands.core import ConditionalChange -from glide.constants import TOK, TJsonResponse -from glide.protobuf.redis_request_pb2 import RequestType -from glide.redis_client import TRedisClient +from glide.constants import TOK, TEncodable, TJsonResponse +from glide.glide_client import TGlideClient +from glide.protobuf.command_request_pb2 import RequestType class JsonGetOptions: """ - Represents options for formatting JSON data, to be used in the [JSON.GET](https://redis.io/commands/json.get/) command. + Represents options for formatting JSON data, to be used in the [JSON.GET](https://valkey.io/commands/json.get/) command. Args: indent (Optional[str]): Sets an indentation string for nested levels. Defaults to None. @@ -55,23 +55,23 @@ def get_options(self) -> List[str]: async def set( - client: TRedisClient, - key: str, - path: str, - value: str, + client: TGlideClient, + key: TEncodable, + path: TEncodable, + value: TEncodable, set_condition: Optional[ConditionalChange] = None, ) -> Optional[TOK]: """ Sets the JSON value at the specified `path` stored at `key`. - See https://redis.io/commands/json.set/ for more details. + See https://valkey.io/commands/json.set/ for more details. Args: - client (TRedisClient): The Redis client to execute the command. - key (str): The key of the JSON document. - path (str): Represents the path within the JSON document where the value will be set. + client (TGlideClient): The Redis client to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): Represents the path within the JSON document where the value will be set. The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added. - value (set): The value to set at the specific path, in JSON formatted str. + value (TEncodable): The value to set at the specific path, in JSON formatted bytes or str. set_condition (Optional[ConditionalChange]): Set the value only if the given condition is met (within the key or path). Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None. @@ -95,64 +95,64 @@ async def set( async def get( - client: TRedisClient, - key: str, - paths: Optional[Union[str, List[str]]] = None, + client: TGlideClient, + key: TEncodable, + paths: Optional[Union[TEncodable, List[TEncodable]]] = None, options: Optional[JsonGetOptions] = None, -) -> Optional[str]: +) -> Optional[bytes]: """ Retrieves the JSON value at the specified `paths` stored at `key`. - See https://redis.io/commands/json.get/ for more details. + See https://valkey.io/commands/json.get/ for more details. Args: - client (TRedisClient): The Redis client to execute the command. - key (str): The key of the JSON document. - paths (Optional[Union[str, List[str]]]): The path or list of paths within the JSON document. Default is root `$`. - options (Optional[JsonGetOptions]): Options for formatting the string representation of the JSON data. See `JsonGetOptions`. + client (TGlideClient): The Redis client to execute the command. + key (TEncodable): The key of the JSON document. + paths (Optional[Union[TEncodable, List[TEncodable]]]): The path or list of paths within the JSON document. Default is root `$`. + options (Optional[JsonGetOptions]): Options for formatting the byte representation of the JSON data. See `JsonGetOptions`. Returns: - str: A bulk string representation of the returned value. + bytes: A bytes representation of the returned value. If `key` doesn't exists, returns None. Examples: >>> from glide import json as redisJson >>> import json >>> json_str = await redisJson.get(client, "doc", "$") - >>> json.loads(json_str) # Parse JSON string to Python data + >>> json.loads(str(json_str)) # Parse JSON string to Python data [{"a": 1.0, "b" :2}] # JSON object retrieved from the key `doc` using json.loads() >>> await redisJson.get(client, "doc", "$") - "[{\"a\":1.0,\"b\":2}]" # Returns the value at path '$' in the JSON document stored at `doc`. + b"[{\"a\":1.0,\"b\":2}]" # Returns the value at path '$' in the JSON document stored at `doc`. >>> await redisJson.get(client, "doc", ["$.a", "$.b"], json.JsonGetOptions(indent=" ", newline="\n", space=" ")) - "{\n \"$.a\": [\n 1.0\n ],\n \"$.b\": [\n 2\n ]\n}" # Returns the values at paths '$.a' and '$.b' in the JSON document stored at `doc`, with specified formatting options. + b"{\n \"$.a\": [\n 1.0\n ],\n \"$.b\": [\n 2\n ]\n}" # Returns the values at paths '$.a' and '$.b' in the JSON document stored at `doc`, with specified formatting options. >>> await redisJson.get(client, "doc", "$.non_existing_path") - "[]" # Returns an empty array since the path '$.non_existing_path' does not exist in the JSON document stored at `doc`. + b"[]" # Returns an empty array since the path '$.non_existing_path' does not exist in the JSON document stored at `doc`. """ args = ["JSON.GET", key] if options: args.extend(options.get_options()) if paths: - if isinstance(paths, str): + if isinstance(paths, (str, bytes)): paths = [paths] args.extend(paths) - return cast(str, await client.custom_command(args)) + return cast(bytes, await client.custom_command(args)) async def delete( - client: TRedisClient, - key: str, - path: Optional[str] = None, + client: TGlideClient, + key: TEncodable, + path: Optional[TEncodable] = None, ) -> int: """ Deletes the JSON value at the specified `path` within the JSON document stored at `key`. - See https://redis.io/commands/json.del/ for more details. + See https://valkey.io/commands/json.del/ for more details. Args: - client (TRedisClient): The Redis client to execute the command. - key (str): The key of the JSON document. - path (Optional[str]): Represents the path within the JSON document where the value will be deleted. + client (TGlideClient): The Redis client to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): Represents the path within the JSON document where the value will be deleted. If None, deletes the entire JSON document at `key`. Defaults to None. Returns: @@ -177,19 +177,19 @@ async def delete( async def forget( - client: TRedisClient, - key: str, - path: Optional[str] = None, + client: TGlideClient, + key: TEncodable, + path: Optional[TEncodable] = None, ) -> Optional[int]: """ Deletes the JSON value at the specified `path` within the JSON document stored at `key`. - See https://redis.io/commands/json.forget/ for more details. + See https://valkey.io/commands/json.forget/ for more details. Args: - client (TRedisClient): The Redis client to execute the command. - key (str): The key of the JSON document. - path (Optional[str]): Represents the path within the JSON document where the value will be deleted. + client (TGlideClient): The Redis client to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): Represents the path within the JSON document where the value will be deleted. If None, deletes the entire JSON document at `key`. Defaults to None. Returns: @@ -215,19 +215,19 @@ async def forget( async def toggle( - client: TRedisClient, - key: str, - path: str, + client: TGlideClient, + key: TEncodable, + path: TEncodable, ) -> TJsonResponse[bool]: """ Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. - See https://redis.io/commands/json.toggle/ for more details. + See https://valkey.io/commands/json.toggle/ for more details. Args: - client (TRedisClient): The Redis client to execute the command. - key (str): The key of the JSON document. - path (str): The JSONPath to specify. + client (TGlideClient): The Redis client to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The JSONPath to specify. Returns: TJsonResponse[bool]: For JSONPath (`path` starts with `$`), returns a list of boolean replies for every possible path, with the toggled boolean value, diff --git a/python/python/glide/async_commands/sorted_set.py b/python/python/glide/async_commands/sorted_set.py index 83c6037341..76ec384712 100644 --- a/python/python/glide/async_commands/sorted_set.py +++ b/python/python/glide/async_commands/sorted_set.py @@ -1,7 +1,10 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from enum import Enum -from typing import List, Optional, Union +from typing import List, Optional, Tuple, Union, cast + +from glide.async_commands.command_args import Limit, OrderBy +from glide.constants import TEncodable class InfBound(Enum): @@ -23,6 +26,43 @@ class InfBound(Enum): """ +class AggregationType(Enum): + """ + Enumeration representing aggregation types for `ZINTERSTORE` and `ZUNIONSTORE` sorted set commands. + """ + + SUM = "SUM" + """ + Represents aggregation by summing the scores of elements across inputs where they exist. + """ + MIN = "MIN" + """ + Represents aggregation by selecting the minimum score of an element across inputs where it exists. + """ + MAX = "MAX" + """ + Represents aggregation by selecting the maximum score of an element across inputs where it exists. + """ + + +class ScoreFilter(Enum): + """ + Defines which elements to pop from a sorted set. + + ScoreFilter is a mandatory option for ZMPOP (https://valkey.io/commands/zmpop) + and BZMPOP (https://valkey.io/commands/bzmpop). + """ + + MIN = "MIN" + """ + Pop elements with the lowest scores. + """ + MAX = "MAX" + """ + Pop elements with the highest scores. + """ + + class ScoreBoundary: """ Represents a specific numeric score boundary in a sorted set. @@ -33,7 +73,7 @@ class ScoreBoundary: """ def __init__(self, value: float, is_inclusive: bool = True): - # Convert the score boundary to the Redis protocol format + # Convert the score boundary to Valkey protocol format self.value = str(value) if is_inclusive else f"({value}" @@ -47,27 +87,10 @@ class LexBoundary: """ def __init__(self, value: str, is_inclusive: bool = True): - # Convert the lexicographic boundary to the Redis protocol format + # Convert the lexicographic boundary to Valkey protocol format self.value = f"[{value}" if is_inclusive else f"({value}" -class Limit: - """ - Represents a limit argument for a range query in a sorted set to be used in [ZRANGE](https://redis.io/commands/zrange) command. - - The optional LIMIT argument can be used to obtain a sub-range from the matching elements - (similar to SELECT LIMIT offset, count in SQL). - Args: - offset (int): The offset from the start of the range. - count (int): The number of elements to include in the range. - A negative count returns all elements from the offset. - """ - - def __init__(self, offset: int, count: int): - self.offset = offset - self.count = count - - class RangeByIndex: """ Represents a range by index (rank) in a sorted set. @@ -132,13 +155,138 @@ def __init__( self.limit = limit +class GeospatialData: + def __init__(self, longitude: float, latitude: float): + """ + Represents a geographic position defined by longitude and latitude. + + The exact limits, as specified by EPSG:900913 / EPSG:3785 / OSGEO:41001 are the following: + - Valid longitudes are from -180 to 180 degrees. + - Valid latitudes are from -85.05112878 to 85.05112878 degrees. + + Args: + longitude (float): The longitude coordinate. + latitude (float): The latitude coordinate. + """ + self.longitude = longitude + self.latitude = latitude + + +class GeoUnit(Enum): + """ + Enumeration representing distance units options for the `GEODIST` command. + """ + + METERS = "m" + """ + Represents distance in meters. + """ + KILOMETERS = "km" + """ + Represents distance in kilometers. + """ + MILES = "mi" + """ + Represents distance in miles. + """ + FEET = "ft" + """ + Represents distance in feet. + """ + + +class GeoSearchByRadius: + """ + Represents search criteria of searching within a certain radius from a specified point. + + Args: + radius (float): Radius of the search area. + unit (GeoUnit): Unit of the radius. See `GeoUnit`. + """ + + def __init__(self, radius: float, unit: GeoUnit): + """ + Initialize the search criteria. + """ + self.radius = radius + self.unit = unit + + def to_args(self) -> List[str]: + """ + Convert the search criteria to the corresponding part of the command. + + Returns: + List[str]: List representation of the search criteria. + """ + return ["BYRADIUS", str(self.radius), self.unit.value] + + +class GeoSearchByBox: + """ + Represents search criteria of searching within a specified rectangular area. + + Args: + width (float): Width of the bounding box. + height (float): Height of the bounding box + unit (GeoUnit): Unit of the radius. See `GeoUnit`. + """ + + def __init__(self, width: float, height: float, unit: GeoUnit): + """ + Initialize the search criteria. + """ + self.width = width + self.height = height + self.unit = unit + + def to_args(self) -> List[str]: + """ + Convert the search criteria to the corresponding part of the command. + + Returns: + List[str]: List representation of the search criteria. + """ + return ["BYBOX", str(self.width), str(self.height), self.unit.value] + + +class GeoSearchCount: + """ + Represents the count option for limiting the number of results in a GeoSearch. + + Args: + count (int): The maximum number of results to return. + any_option (bool): Whether to allow returning as enough matches are found. + This means that the results returned may not be the ones closest to the specified point. Default to False. + """ + + def __init__(self, count: int, any_option: bool = False): + """ + Initialize the count option. + """ + self.count = count + self.any_option = any_option + + def to_args(self) -> List[str]: + """ + Convert the count option to the corresponding part of the command. + + Returns: + List[str]: List representation of the count option. + """ + if self.any_option: + return ["COUNT", str(self.count), "ANY"] + return ["COUNT", str(self.count)] + + def _create_zrange_args( - key: str, + key: TEncodable, range_query: Union[RangeByLex, RangeByScore, RangeByIndex], reverse: bool, with_scores: bool, -) -> List[str]: - args = [key, str(range_query.start), str(range_query.stop)] + destination: Optional[TEncodable] = None, +) -> List[TEncodable]: + args = [destination] if destination else [] + args += [key, str(range_query.start), str(range_query.stop)] if isinstance(range_query, RangeByScore): args.append("BYSCORE") @@ -158,3 +306,97 @@ def _create_zrange_args( args.append("WITHSCORES") return args + + +def separate_keys( + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]] +) -> Tuple[List[TEncodable], List[TEncodable]]: + """ + Returns separate lists of keys and weights in case of weighted keys. + """ + if not keys: + return [], [] + + key_list: List[TEncodable] = [] + weight_list: List[TEncodable] = [] + + if isinstance(keys[0], tuple): + for item in keys: + key = item[0] + weight = item[1] + key_list.append(cast(TEncodable, key)) + weight_list.append(cast(TEncodable, str(weight))) + else: + key_list.extend(cast(List[TEncodable], keys)) + + return key_list, weight_list + + +def _create_zinter_zunion_cmd_args( + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + destination: Optional[TEncodable] = None, +) -> List[TEncodable]: + args: List[TEncodable] = [] + + if destination: + args.append(destination) + + args.append(str(len(keys))) + + only_keys, weights = separate_keys(keys) + + args.extend(only_keys) + + if weights: + args.append("WEIGHTS") + args.extend(weights) + + if aggregation_type: + args.append("AGGREGATE") + args.append(aggregation_type.value) + + return args + + +def _create_geosearch_args( + keys: List[TEncodable], + search_from: Union[str, bytes, GeospatialData], + search_by: Union[GeoSearchByRadius, GeoSearchByBox], + order_by: Optional[OrderBy] = None, + count: Optional[GeoSearchCount] = None, + with_coord: bool = False, + with_dist: bool = False, + with_hash: bool = False, + store_dist: bool = False, +) -> List[TEncodable]: + args: List[TEncodable] = keys + if isinstance(search_from, (str, bytes)): + args.extend(["FROMMEMBER", search_from]) + else: + args.extend( + [ + "FROMLONLAT", + str(search_from.longitude), + str(search_from.latitude), + ] + ) + + args.extend(search_by.to_args()) + + if order_by: + args.append(order_by.value) + if count: + args.extend(count.to_args()) + + if with_coord: + args.append("WITHCOORD") + if with_dist: + args.append("WITHDIST") + if with_hash: + args.append("WITHHASH") + + if store_dist: + args.append("STOREDIST") + + return args diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index 1c50eb38b3..37815e14c4 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -1,39 +1,55 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from __future__ import annotations -from typing import Dict, List, Mapping, Optional, cast - -from glide.async_commands.core import CoreCommands, InfoSection -from glide.async_commands.transaction import BaseTransaction, Transaction -from glide.constants import TOK, TResult -from glide.protobuf.redis_request_pb2 import RequestType +from typing import Any, Dict, List, Mapping, Optional, Set, Union, cast + +from glide.async_commands.command_args import Limit, ObjectType, OrderBy +from glide.async_commands.core import ( + CoreCommands, + FlushMode, + FunctionRestorePolicy, + InfoSection, + _build_sort_args, +) +from glide.async_commands.transaction import Transaction +from glide.constants import ( + OK, + TOK, + TEncodable, + TFunctionListResponse, + TFunctionStatsResponse, + TResult, +) +from glide.protobuf.command_request_pb2 import RequestType class StandaloneCommands(CoreCommands): - async def custom_command(self, command_args: List[str]) -> TResult: + async def custom_command(self, command_args: List[TEncodable]) -> TResult: """ Executes a single command, without checking inputs. - @remarks - This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + See the [Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + for details on the restrictions and limitations of the custom command API. + @example - Return a list of all pub/sub clients: connection.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"]) Args: - command_args (List[str]): List of strings of the command's arguments. + command_args (List[TEncodable]): List of the command's arguments, where each argument is either a string or bytes. Every part of the command, including the command name and subcommands, should be added as a separate value in args. Returns: - TResult: The returning value depends on the executed command and the route + TResult: The returning value depends on the executed command. """ return await self._execute_command(RequestType.CustomCommand, command_args) async def info( self, sections: Optional[List[InfoSection]] = None, - ) -> str: + ) -> bytes: """ - Get information and statistics about the Redis server. - See https://redis.io/commands/info/ for details. + Get information and statistics about the server. + See https://valkey.io/commands/info/ for details. Args: sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of @@ -41,35 +57,37 @@ async def info( Returns: - str: Returns a string containing the information for the sections requested. + bytes: Returns bytes containing the information for the sections requested. """ - args = [section.value for section in sections] if sections else [] - return cast(str, await self._execute_command(RequestType.Info, args)) + args: List[TEncodable] = ( + [section.value for section in sections] if sections else [] + ) + return cast(bytes, await self._execute_command(RequestType.Info, args)) async def exec( self, - transaction: BaseTransaction | Transaction, + transaction: Transaction, ) -> Optional[List[TResult]]: """ Execute a transaction by processing the queued commands. - See https://redis.io/topics/Transactions/ for details on Redis Transactions. + See https://valkey.io/docs/topics/transactions/ for details on Transactions. Args: - transaction (Transaction): A Transaction object containing a list of commands to be executed. + transaction (Transaction): A `Transaction` object containing a list of commands to be executed. Returns: Optional[List[TResult]]: A list of results corresponding to the execution of each command - in the transaction. If a command returns a value, it will be included in the list. If a command - doesn't return a value, the list entry will be None. - If the transaction failed due to a WATCH command, `exec` will return `None`. + in the transaction. If a command returns a value, it will be included in the list. If a command + doesn't return a value, the list entry will be `None`. + If the transaction failed due to a WATCH command, `exec` will return `None`. """ commands = transaction.commands[:] return await self._execute_transaction(commands) async def select(self, index: int) -> TOK: """ - Change the currently selected Redis database. - See https://redis.io/commands/select/ for details. + Change the currently selected database. + See https://valkey.io/commands/select/ for details. Args: index (int): The index of the database to select. @@ -81,8 +99,8 @@ async def select(self, index: int) -> TOK: async def config_resetstat(self) -> TOK: """ - Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - See https://redis.io/commands/config-resetstat/ for details. + Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands. + See https://valkey.io/commands/config-resetstat/ for details. Returns: OK: Returns "OK" to confirm that the statistics were successfully reset. @@ -92,7 +110,7 @@ async def config_resetstat(self) -> TOK: async def config_rewrite(self) -> TOK: """ Rewrite the configuration file with the current configuration. - See https://redis.io/commands/config-rewrite/ for details. + See https://valkey.io/commands/config-rewrite/ for details. Returns: OK: OK is returned when the configuration was rewritten properly. Otherwise, an error is raised. @@ -104,64 +122,63 @@ async def client_id( ) -> int: """ Returns the current connection id. - See https://redis.io/commands/client-id/ for more information. + See https://valkey.io/commands/client-id/ for more information. Returns: int: the id of the client. """ return cast(int, await self._execute_command(RequestType.ClientId, [])) - async def ping(self, message: Optional[str] = None) -> str: + async def ping(self, message: Optional[TEncodable] = None) -> bytes: """ - Ping the Redis server. - See https://redis.io/commands/ping/ for more details. + Ping the server. + See https://valkey.io/commands/ping/ for more details. Args: - message (Optional[str]): An optional message to include in the PING command. If not provided, - the server will respond with "PONG". If provided, the server will respond with a copy of the message. + message (Optional[TEncodable]): An optional message to include in the PING command. If not provided, + the server will respond with b"PONG". If provided, the server will respond with a copy of the message. Returns: - str: "PONG" if `message` is not provided, otherwise return a copy of `message`. + bytes: b"PONG" if `message` is not provided, otherwise return a copy of `message`. Examples: >>> await client.ping() - "PONG" + b"PONG" >>> await client.ping("Hello") - "Hello" + b"Hello" """ argument = [] if message is None else [message] - return cast(str, await self._execute_command(RequestType.Ping, argument)) + return cast(bytes, await self._execute_command(RequestType.Ping, argument)) - async def config_get(self, parameters: List[str]) -> Dict[str, str]: + async def config_get(self, parameters: List[TEncodable]) -> Dict[bytes, bytes]: """ Get the values of configuration parameters. - See https://redis.io/commands/config-get/ for details. + See https://valkey.io/commands/config-get/ for details. Args: - parameters (List[str]): A list of configuration parameter names to retrieve values for. + parameters (List[TEncodable]): A list of configuration parameter names to retrieve values for. Returns: - Dict[str, str]: A dictionary of values corresponding to the configuration parameters. + Dict[bytes, bytes]: A dictionary of values corresponding to the configuration parameters. Examples: >>> await client.config_get(["timeout"] , RandomNode()) - {'timeout': '1000'} - >>> await client.config_get(["timeout" , "maxmemory"]) - {'timeout': '1000', "maxmemory": "1GB"} - + {b'timeout': b'1000'} + >>> await client.config_get([b"timeout" , "maxmemory"]) + {b'timeout': b'1000', b'maxmemory': b'1GB'} """ return cast( - Dict[str, str], + Dict[bytes, bytes], await self._execute_command(RequestType.ConfigGet, parameters), ) - async def config_set(self, parameters_map: Mapping[str, str]) -> TOK: + async def config_set(self, parameters_map: Mapping[TEncodable, TEncodable]) -> TOK: """ Set configuration parameters to the specified values. - See https://redis.io/commands/config-set/ for details. + See https://valkey.io/commands/config-set/ for details. Args: - parameters_map (Mapping[str, str]): A map consisting of configuration + parameters_map (Mapping[TEncodable, TEncodable]): A map consisting of configuration parameters and their respective values to set. Returns: @@ -169,34 +186,34 @@ async def config_set(self, parameters_map: Mapping[str, str]) -> TOK: Examples: >>> config_set({"timeout": "1000", "maxmemory": "1GB"}) - OK + OK """ - parameters: List[str] = [] + parameters: List[TEncodable] = [] for pair in parameters_map.items(): parameters.extend(pair) return cast(TOK, await self._execute_command(RequestType.ConfigSet, parameters)) - async def client_getname(self) -> Optional[str]: + async def client_getname(self) -> Optional[bytes]: """ Get the name of the primary's connection. - See https://redis.io/commands/client-getname/ for more details. + See https://valkey.io/commands/client-getname/ for more details. Returns: - Optional[str]: Returns the name of the client connection as a string if a name is set, + Optional[bytes]: Returns the name of the client connection as a byte string if a name is set, or None if no name is assigned. Examples: >>> await client.client_getname() - 'Connection Name' + b'Connection Name' """ return cast( - Optional[str], await self._execute_command(RequestType.ClientGetName, []) + Optional[bytes], await self._execute_command(RequestType.ClientGetName, []) ) async def dbsize(self) -> int: """ Returns the number of keys in the currently selected database. - See https://redis.io/commands/dbsize for more details. + See https://valkey.io/commands/dbsize for more details. Returns: int: The number of keys in the currently selected database. @@ -207,40 +224,792 @@ async def dbsize(self) -> int: """ return cast(int, await self._execute_command(RequestType.DBSize, [])) - async def echo(self, message: str) -> str: + async def echo(self, message: TEncodable) -> bytes: """ Echoes the provided `message` back. - See https://redis.io/commands/echo for more details. + See https://valkey.io/commands/echo for more details. + + Args: + message (TEncodable): The message to be echoed back. + + Returns: + bytes: The provided `message`. + + Examples: + >>> await client.echo("Valkey GLIDE") + b'Valkey GLIDE' + """ + return cast(bytes, await self._execute_command(RequestType.Echo, [message])) + + async def function_load( + self, library_code: TEncodable, replace: bool = False + ) -> bytes: + """ + Loads a library to Valkey. + + See https://valkey.io/commands/function-load/ for more details. + + Args: + library_code (TEncodable): The source code that implements the library. + replace (bool): Whether the given library should overwrite a library with the same name if + it already exists. + + Returns: + bytes: The library name that was loaded. + + Examples: + >>> code = "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + >>> await client.function_load(code, True) + b"mylib" + + Since: Valkey 7.0.0. + """ + return cast( + bytes, + await self._execute_command( + RequestType.FunctionLoad, + ["REPLACE", library_code] if replace else [library_code], + ), + ) + + async def function_list( + self, library_name_pattern: Optional[TEncodable] = None, with_code: bool = False + ) -> TFunctionListResponse: + """ + Returns information about the functions and libraries. + + See https://valkey.io/commands/function-list/ for more details. + + Args: + library_name_pattern (Optional[TEncodable]): A wildcard pattern for matching library names. + with_code (bool): Specifies whether to request the library code from the server or not. + + Returns: + TFunctionListResponse: Info about all or + selected libraries and their functions. + + Examples: + >>> response = await client.function_list("myLib?_backup", True) + [{ + b"library_name": b"myLib5_backup", + b"engine": b"LUA", + b"functions": [{ + b"name": b"myfunc", + b"description": None, + b"flags": {b"no-writes"}, + }], + b"library_code": b"#!lua name=mylib \n sever.register_function('myfunc', function(keys, args) return args[1] end)" + }] + + Since: Valkey 7.0.0. + """ + args = [] + if library_name_pattern is not None: + args.extend(["LIBRARYNAME", library_name_pattern]) + if with_code: + args.append("WITHCODE") + return cast( + TFunctionListResponse, + await self._execute_command( + RequestType.FunctionList, + args, + ), + ) + + async def function_flush(self, mode: Optional[FlushMode] = None) -> TOK: + """ + Deletes all function libraries. + + See https://valkey.io/commands/function-flush/ for more details. + + Args: + mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + + Returns: + TOK: A simple `OK`. + + Examples: + >>> await client.function_flush(FlushMode.SYNC) + "OK" + + Since: Valkey 7.0.0. + """ + return cast( + TOK, + await self._execute_command( + RequestType.FunctionFlush, + [mode.value] if mode else [], + ), + ) + + async def function_delete(self, library_name: TEncodable) -> TOK: + """ + Deletes a library and all its functions. + + See https://valkey.io/commands/function-delete/ for more details. + + Args: + library_code (TEncodable): The library name to delete + + Returns: + TOK: A simple `OK`. + + Examples: + >>> await client.function_delete("my_lib") + "OK" + + Since: Valkey 7.0.0. + """ + return cast( + TOK, + await self._execute_command( + RequestType.FunctionDelete, + [library_name], + ), + ) + + async def function_kill(self) -> TOK: + """ + Kills a function that is currently executing. + This command only terminates read-only functions. + + See https://valkey.io/commands/function-kill/ for more details. + + Returns: + TOK: A simple `OK`. + + Examples: + >>> await client.function_kill() + "OK" + + Since: Valkey 7.0.0. + """ + return cast( + TOK, + await self._execute_command(RequestType.FunctionKill, []), + ) + + async def function_stats(self) -> TFunctionStatsResponse: + """ + Returns information about the function that's currently running and information about the + available execution engines. + + See https://valkey.io/commands/function-stats/ for more details + + Returns: + TFunctionStatsResponse: A `Mapping` with two keys: + - `running_script` with information about the running script. + - `engines` with information about available engines and their stats. + See example for more details. + + Examples: + >>> await client.function_stats() + { + 'running_script': { + 'name': 'foo', + 'command': ['FCALL', 'foo', '0', 'hello'], + 'duration_ms': 7758 + }, + 'engines': { + 'LUA': { + 'libraries_count': 1, + 'functions_count': 1, + } + } + } + + Since: Valkey version 7.0.0. + """ + return cast( + TFunctionStatsResponse, + await self._execute_command(RequestType.FunctionStats, []), + ) + + async def function_dump(self) -> bytes: + """ + Returns the serialized payload of all loaded libraries. + + See https://valkey.io/docs/latest/commands/function-dump/ for more details. + + Returns: + bytes: The serialized payload of all loaded libraries. + + Examples: + >>> payload = await client.function_dump() + # The serialized payload of all loaded libraries. This response can + # be used to restore loaded functions on any Valkey instance. + >>> await client.function_restore(payload) + "OK" # The serialized dump response was used to restore the libraries. + + Since: Valkey 7.0.0. + """ + return cast(bytes, await self._execute_command(RequestType.FunctionDump, [])) + + async def function_restore( + self, payload: TEncodable, policy: Optional[FunctionRestorePolicy] = None + ) -> TOK: + """ + Restores libraries from the serialized payload returned by the `function_dump` command. + + See https://valkey.io/docs/latest/commands/function-restore/ for more details. Args: - message (str): The message to be echoed back. + payload (TEncodable): The serialized data from the `function_dump` command. + policy (Optional[FunctionRestorePolicy]): A policy for handling existing libraries. Returns: - str: The provided `message`. + TOK: OK. Examples: - >>> await client.echo("Glide-for-Redis") - 'Glide-for-Redis' + >>> payload = await client.function_dump() + # The serialized payload of all loaded libraries. This response can + # be used to restore loaded functions on any Valkey instance. + >>> await client.function_restore(payload) + "OK" # The serialized dump response was used to restore the libraries. + >>> await client.function_restore(payload, FunctionRestorePolicy.FLUSH) + "OK" # The serialized dump response was used to restore the libraries with the specified policy. + + Since: Valkey 7.0.0. """ - return cast(str, await self._execute_command(RequestType.Echo, [message])) + args: List[TEncodable] = [payload] + if policy is not None: + args.append(policy.value) - async def time(self) -> List[str]: + return cast(TOK, await self._execute_command(RequestType.FunctionRestore, args)) + + async def time(self) -> List[bytes]: """ Returns the server time. - See https://redis.io/commands/time/ for more details. + See https://valkey.io/commands/time/ for more details. Returns: - List[str]: The current server time as a two items `array`: + List[bytes]: The current server time as a two items `array`: A Unix timestamp and the amount of microseconds already elapsed in the current second. The returned `array` is in a [Unix timestamp, Microseconds already elapsed] format. Examples: >>> await client.time() - ['1710925775', '913580'] + [b'1710925775', b'913580'] """ return cast( - List[str], + List[bytes], await self._execute_command(RequestType.Time, []), ) + + async def lastsave(self) -> int: + """ + Returns the Unix time of the last DB save timestamp or startup timestamp if no save was made since then. + + See https://valkey.io/commands/lastsave for more details. + + Returns: + int: The Unix time of the last successful DB save. + + Examples: + >>> await client.lastsave() + 1710925775 # Unix time of the last DB save + """ + return cast( + int, + await self._execute_command(RequestType.LastSave, []), + ) + + async def move(self, key: TEncodable, db_index: int) -> bool: + """ + Move `key` from the currently selected database to the database specified by `db_index`. + + See https://valkey.io/commands/move/ for more details. + + Args: + key (TEncodable): The key to move. + db_index (int): The index of the database to move `key` to. + + Returns: + bool: True if `key` was moved, or False if the `key` already exists in the destination database + or does not exist in the source database. + + Example: + >>> await client.move("some_key", 1) + True + """ + return cast( + bool, + await self._execute_command(RequestType.Move, [key, str(db_index)]), + ) + + async def sort( + self, + key: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> List[Optional[bytes]]: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + This command is routed to primary nodes only. + To store the result into a new key, see `sort_store`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + by_pattern (Optional[TEncodable]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_patterns (Optional[List[TEncodable]]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point + + Returns: + List[Optional[bytes]]: Returns a list of sorted elements. + + Examples: + >>> await client.lpush("mylist", [b"3", b"1", b"2"]) + >>> await client.sort("mylist") + [b'1', b'2', b'3'] + >>> await client.sort("mylist", order=OrderBy.DESC) + [b'3', b'2', b'1'] + >>> await client.lpush("mylist2", ['2', '1', '2', '3', '3', '1']) + >>> await client.sort("mylist2", limit=Limit(2, 3)) + [b'2', b'2', b'3'] + >>> await client.hset("user:1": {"name": "Alice", "age": '30'}) + >>> await client.hset("user:2", {"name": "Bob", "age": '25'}) + >>> await client.lpush("user_ids", ['2', '1']) + >>> await client.sort("user_ids", by_pattern="user:*->age", get_patterns=["user:*->name"]) + [b'Bob', b'Alice'] + """ + args = _build_sort_args(key, by_pattern, limit, get_patterns, order, alpha) + result = await self._execute_command(RequestType.Sort, args) + return cast(List[Optional[bytes]], result) + + async def sort_ro( + self, + key: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> List[Optional[bytes]]: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort_ro` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + This command is routed depending on the client's `ReadFrom` strategy. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + by_pattern (Optional[TEncodable]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[TEncodable]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point + + Returns: + List[Optional[bytes]]: Returns a list of sorted elements. + + Examples: + >>> await client.lpush("mylist", 3, 1, 2) + >>> await client.sort_ro("mylist") + [b'1', b'2', b'3'] + >>> await client.sort_ro("mylist", order=OrderBy.DESC) + [b'3', b'2', b'1'] + >>> await client.lpush("mylist2", 2, 1, 2, 3, 3, 1) + >>> await client.sort_ro("mylist2", limit=Limit(2, 3)) + [b'2', b'2', b'3'] + >>> await client.hset("user:1", "name", "Alice", "age", 30) + >>> await client.hset("user:2", "name", "Bob", "age", 25) + >>> await client.lpush("user_ids", 2, 1) + >>> await client.sort_ro("user_ids", by_pattern="user:*->age", get_patterns=["user:*->name"]) + [b'Bob', b'Alice'] + + Since: Valkey version 7.0.0. + """ + args = _build_sort_args(key, by_pattern, limit, get_patterns, order, alpha) + result = await self._execute_command(RequestType.SortReadOnly, args) + return cast(List[Optional[bytes]], result) + + async def sort_store( + self, + key: TEncodable, + destination: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> int: + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + The `sort` command can be used to sort elements based on different criteria, apply transformations on sorted elements, and store the result in a new key. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + destination (TEncodable): The key where the sorted result will be stored. + by_pattern (Optional[TEncodable]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_patterns (Optional[List[TEncodable]]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point + + Returns: + int: The number of elements in the sorted key stored at `store`. + + Examples: + >>> await client.lpush("mylist", ['3', '1', '2']) + >>> await client.sort_store("mylist", "sorted_list") + 3 # Indicates that the sorted list "sorted_list" contains three elements. + >>> await client.lrange("sorted_list", 0, -1) + [b'1', b'2', b'3'] + """ + args = _build_sort_args( + key, by_pattern, limit, get_patterns, order, alpha, store=destination + ) + result = await self._execute_command(RequestType.Sort, args) + return cast(int, result) + + async def publish(self, message: TEncodable, channel: TEncodable) -> int: + """ + Publish a message on pubsub channel. + See https://valkey.io/commands/publish for more details. + + Args: + message (TEncodable): Message to publish + channel (TEncodable): Channel to publish the message on. + + Returns: + int: Number of subscriptions in primary node that received the message. + Note that this value does not include subscriptions that configured on replicas. + + Examples: + >>> await client.publish("Hi all!", "global-channel") + 1 # This message was posted to 1 subscription which is configured on primary node + """ + return cast( + int, await self._execute_command(RequestType.Publish, [channel, message]) + ) + + async def flushall(self, flush_mode: Optional[FlushMode] = None) -> TOK: + """ + Deletes all the keys of all the existing databases. This command never fails. + + See https://valkey.io/commands/flushall for more details. + + Args: + flush_mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + + Returns: + TOK: A simple OK response. + + Examples: + >>> await client.flushall(FlushMode.ASYNC) + OK # This command never fails. + """ + args: List[TEncodable] = [] + if flush_mode is not None: + args.append(flush_mode.value) + + return cast( + TOK, + await self._execute_command(RequestType.FlushAll, args), + ) + + async def flushdb(self, flush_mode: Optional[FlushMode] = None) -> TOK: + """ + Deletes all the keys of the currently selected database. This command never fails. + + See https://valkey.io/commands/flushdb for more details. + + Args: + flush_mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + + Returns: + TOK: A simple OK response. + + Examples: + >>> await client.flushdb() + OK # The keys of the currently selected database were deleted. + >>> await client.flushdb(FlushMode.ASYNC) + OK # The keys of the currently selected database were deleted asynchronously. + """ + args: List[TEncodable] = [] + if flush_mode is not None: + args.append(flush_mode.value) + + return cast( + TOK, + await self._execute_command(RequestType.FlushDB, args), + ) + + async def copy( + self, + source: TEncodable, + destination: TEncodable, + destinationDB: Optional[int] = None, + replace: Optional[bool] = None, + ) -> bool: + """ + Copies the value stored at the `source` to the `destination` key. If `destinationDB` + is specified, the value will be copied to the database specified by `destinationDB`, + otherwise the current database will be used. When `replace` is True, removes the + `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Args: + source (TEncodable): The key to the source value. + destination (TEncodable): The key where the value should be copied to. + destinationDB (Optional[int]): The alternative logical database index for the destination key. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Returns: + bool: True if the source was copied. Otherwise, return False. + + Examples: + >>> await client.set("source", "sheep") + >>> await client.copy(b"source", b"destination", 1, False) + True # Source was copied + >>> await client.select(1) + >>> await client.get("destination") + b"sheep" + + Since: Valkey version 6.2.0. + """ + args: List[TEncodable] = [source, destination] + if destinationDB is not None: + args.extend(["DB", str(destinationDB)]) + if replace is True: + args.append("REPLACE") + return cast( + bool, + await self._execute_command(RequestType.Copy, args), + ) + + async def lolwut( + self, + version: Optional[int] = None, + parameters: Optional[List[int]] = None, + ) -> bytes: + """ + Displays a piece of generative computer art and the Valkey version. + + See https://valkey.io/commands/lolwut for more details. + + Args: + version (Optional[int]): Version of computer art to generate. + parameters (Optional[List[int]]): Additional set of arguments in order to change the output: + For version `5`, those are length of the line, number of squares per row, and number of squares per column. + For version `6`, those are number of columns and number of lines. + + Returns: + bytes: A piece of generative computer art along with the current Valkey version. + + Examples: + >>> await client.lolwut(6, [40, 20]); + b"Redis ver. 7.2.3" # Indicates the current Valkey version + >>> await client.lolwut(5, [30, 5, 5]); + b"Redis ver. 7.2.3" # Indicates the current Valkey version + """ + args: List[TEncodable] = [] + if version is not None: + args.extend(["VERSION", str(version)]) + if parameters: + for var in parameters: + args.extend(str(var)) + return cast( + bytes, + await self._execute_command(RequestType.Lolwut, args), + ) + + async def random_key(self) -> Optional[bytes]: + """ + Returns a random existing key name from the currently selected database. + + See https://valkey.io/commands/randomkey for more details. + + Returns: + Optional[bytes]: A random existing key name from the currently selected database. + + Examples: + >>> await client.random_key() + b"random_key_name" # "random_key_name" is a random existing key name from the currently selected database. + """ + return cast( + Optional[bytes], + await self._execute_command(RequestType.RandomKey, []), + ) + + async def wait( + self, + numreplicas: int, + timeout: int, + ) -> int: + """ + Blocks the current client until all the previous write commands are successfully transferred + and acknowledged by at least `numreplicas` of replicas. If `timeout` is + reached, the command returns even if the specified number of replicas were not yet reached. + + See https://valkey.io/commands/wait for more details. + + Args: + numreplicas (int): The number of replicas to reach. + timeout (int): The timeout value specified in milliseconds. A value of 0 will block indefinitely. + + Returns: + int: The number of replicas reached by all the writes performed in the context of the current connection. + + Examples: + >>> await client.set("key", "value"); + >>> await client.wait(1, 1000); + // return 1 when a replica is reached or 0 if 1000ms is reached. + """ + args: List[TEncodable] = [str(numreplicas), str(timeout)] + return cast( + int, + await self._execute_command(RequestType.Wait, args), + ) + + async def unwatch(self) -> TOK: + """ + Flushes all the previously watched keys for a transaction. Executing a transaction will + automatically flush all previously watched keys. + + See https://valkey.io/commands/unwatch for more details. + + Returns: + TOK: A simple "OK" response. + + Examples: + >>> await client.watch("sampleKey") + 'OK' + >>> await client.unwatch() + 'OK' + """ + return cast( + TOK, + await self._execute_command(RequestType.UnWatch, []), + ) + + async def scan( + self, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + type: Optional[ObjectType] = None, + ) -> List[Union[bytes, List[bytes]]]: + """ + Incrementally iterate over a collection of keys. + SCAN is a cursor based iterator. This means that at every call of the command, + the server returns an updated cursor that the user needs to use as the cursor argument in the next call. + An iteration starts when the cursor is set to "0", and terminates when the cursor returned by the server is "0". + + A full iteration always retrieves all the elements that were present + in the collection from the start to the end of a full iteration. + Elements that were not constantly present in the collection during a full iteration, may be returned or not. + + See https://valkey.io/commands/scan for more details. + + Args: + cursor (TResult): The cursor used for iteration. For the first iteration, the cursor should be set to "0". + Using a non-zero cursor in the first iteration, + or an invalid cursor at any iteration, will lead to undefined results. + Using the same cursor in multiple iterations will, in case nothing changed between the iterations, + return the same elements multiple times. + If the the db has changed, it may result an undefined behavior. + match (Optional[TResult]): A pattern to match keys against. + count (Optional[int]): The number of keys to return per iteration. + The number of keys returned per iteration is not guaranteed to be the same as the count argument. + the argument is used as a hint for the server to know how many "steps" it can use to retrieve the keys. + The default value is 10. + type (ObjectType): The type of object to scan for. + + Returns: + List[Union[bytes, List[bytes]]]: A List containing the next cursor value and a list of keys, + formatted as [cursor, [key1, key2, ...]] + + Examples: + >>> result = await client.scan(b'0') + print(result) #[b'17', [b'key1', b'key2', b'key3', b'key4', b'key5', b'set1', b'set2', b'set3']] + first_cursor_result = result[0] + result = await client.scan(first_cursor_result) + print(result) #[b'349', [b'key4', b'key5', b'set1', b'hash1', b'zset1', b'list1', b'list2', + b'list3', b'zset2', b'zset3', b'zset4', b'zset5', b'zset6']] + result = await client.scan(result[0]) + print(result) #[b'0', [b'key6', b'key7']] + + >>> result = await client.scan(first_cursor_result, match=b'key*', count=2) + print(result) #[b'6', [b'key4', b'key5']] + + >>> result = await client.scan("0", type=ObjectType.Set) + print(result) #[b'362', [b'set1', b'set2', b'set3']] + """ + args = [cursor] + if match: + args.extend(["MATCH", match]) + if count: + args.extend(["COUNT", str(count)]) + if type: + args.extend(["TYPE", type.value]) + return cast( + List[Union[bytes, List[bytes]]], + await self._execute_command(RequestType.Scan, args), + ) diff --git a/python/python/glide/async_commands/stream.py b/python/python/glide/async_commands/stream.py new file mode 100644 index 0000000000..33f6ff6224 --- /dev/null +++ b/python/python/glide/async_commands/stream.py @@ -0,0 +1,442 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import List, Optional, Union + +from glide.constants import TEncodable + + +class StreamTrimOptions(ABC): + """ + Abstract base class for stream trim options. + """ + + @abstractmethod + def __init__( + self, + exact: bool, + threshold: Union[TEncodable, int], + method: str, + limit: Optional[int] = None, + ): + """ + Initialize stream trim options. + + Args: + exact (bool): If `true`, the stream will be trimmed exactly. + Otherwise the stream will be trimmed in a near-exact manner, which is more efficient. + threshold (Union[TEncodable, int]): Threshold for trimming. + method (str): Method for trimming (e.g., MINID, MAXLEN). + limit (Optional[int]): Max number of entries to be trimmed. Defaults to None. + Note: If `exact` is set to `True`, `limit` cannot be specified. + """ + if exact and limit: + raise ValueError( + "If `exact` is set to `True`, `limit` cannot be specified." + ) + self.exact = exact + self.threshold = threshold + self.method = method + self.limit = limit + + def to_args(self) -> List[str]: + """ + Convert options to arguments for the command. + + Returns: + List[str]: List of arguments for the command. + """ + option_args = [ + self.method, + "=" if self.exact else "~", + str(self.threshold), + ] + if self.limit is not None: + option_args.extend(["LIMIT", str(self.limit)]) + return option_args + + +class TrimByMinId(StreamTrimOptions): + """ + Stream trim option to trim by minimum ID. + """ + + def __init__(self, exact: bool, threshold: TEncodable, limit: Optional[int] = None): + """ + Initialize trim option by minimum ID. + + Args: + exact (bool): If `true`, the stream will be trimmed exactly. + Otherwise the stream will be trimmed in a near-exact manner, which is more efficient. + threshold (TEncodable): Threshold for trimming by minimum ID. + limit (Optional[int]): Max number of entries to be trimmed. Defaults to None. + Note: If `exact` is set to `True`, `limit` cannot be specified. + """ + super().__init__(exact, threshold, "MINID", limit) + + +class TrimByMaxLen(StreamTrimOptions): + """ + Stream trim option to trim by maximum length. + """ + + def __init__(self, exact: bool, threshold: int, limit: Optional[int] = None): + """ + Initialize trim option by maximum length. + + Args: + exact (bool): If `true`, the stream will be trimmed exactly. + Otherwise the stream will be trimmed in a near-exact manner, which is more efficient. + threshold (int): Threshold for trimming by maximum length. + limit (Optional[int]): Max number of entries to be trimmed. Defaults to None. + Note: If `exact` is set to `True`, `limit` cannot be specified. + """ + super().__init__(exact, threshold, "MAXLEN", limit) + + +class StreamAddOptions: + """ + Options for adding entries to a stream. + """ + + def __init__( + self, + id: Optional[TEncodable] = None, + make_stream: bool = True, + trim: Optional[StreamTrimOptions] = None, + ): + """ + Initialize stream add options. + + Args: + id (Optional[TEncodable]): ID for the new entry. If set, the new entry will be added with this ID. If not specified, '*' is used. + make_stream (bool, optional): If set to False, a new stream won't be created if no stream matches the given key. + trim (Optional[StreamTrimOptions]): If set, the add operation will also trim the older entries in the stream. See `StreamTrimOptions`. + """ + self.id = id + self.make_stream = make_stream + self.trim = trim + + def to_args(self) -> List[TEncodable]: + """ + Convert options to arguments for the command. + + Returns: + List[str]: List of arguments for the command. + """ + option_args: List[TEncodable] = [] + if not self.make_stream: + option_args.append("NOMKSTREAM") + if self.trim: + option_args.extend(self.trim.to_args()) + option_args.append(self.id if self.id else "*") + + return option_args + + +class StreamRangeBound(ABC): + """ + Abstract Base Class used in the `XPENDING`, `XRANGE`, and `XREVRANGE` commands to specify the starting and ending + range bound for the stream search by stream entry ID. + """ + + @abstractmethod + def to_arg(self) -> TEncodable: + """ + Returns the stream range bound as a string argument to be used in the `XRANGE` or `XREVRANGE` commands. + """ + pass + + +class MinId(StreamRangeBound): + """ + Stream ID boundary used to specify the minimum stream entry ID. Can be used in the `XRANGE` or `XREVRANGE` commands + to get the first stream ID. + """ + + MIN_RANGE_VALKEY_API = "-" + + def to_arg(self) -> str: + return self.MIN_RANGE_VALKEY_API + + +class MaxId(StreamRangeBound): + """ + Stream ID boundary used to specify the maximum stream entry ID. Can be used in the `XRANGE` or `XREVRANGE` commands + to get the last stream ID. + """ + + MAX_RANGE_VALKEY_API = "+" + + def to_arg(self) -> str: + return self.MAX_RANGE_VALKEY_API + + +class IdBound(StreamRangeBound): + """ + Inclusive (closed) stream ID boundary used to specify a range of IDs to search. Stream ID bounds can be complete + with a timestamp and sequence number separated by a dash ("-"), for example "1526985054069-0". Stream ID bounds can + also be incomplete, with just a timestamp. + """ + + @staticmethod + def from_timestamp(timestamp: int) -> IdBound: + """ + Creates an incomplete stream ID boundary without the sequence number for a range search. + + Args: + timestamp (int): The stream ID timestamp. + """ + return IdBound(str(timestamp)) + + def __init__(self, stream_id: TEncodable): + """ + Creates a stream ID boundary for a range search. + + Args: + stream_id (str): The stream ID. + """ + self.stream_id = stream_id + + def to_arg(self) -> TEncodable: + return self.stream_id + + +class ExclusiveIdBound(StreamRangeBound): + """ + Exclusive (open) stream ID boundary used to specify a range of IDs to search. Stream ID bounds can be complete with + a timestamp and sequence number separated by a dash ("-"), for example "1526985054069-0". Stream ID bounds can also + be incomplete, with just a timestamp. + + Since: Valkey version 6.2.0. + """ + + EXCLUSIVE_BOUND_VALKEY_API = "(" + + @staticmethod + def from_timestamp(timestamp: int) -> ExclusiveIdBound: + """ + Creates an incomplete stream ID boundary without the sequence number for a range search. + + Args: + timestamp (int): The stream ID timestamp. + """ + return ExclusiveIdBound(str(timestamp)) + + def __init__(self, stream_id: TEncodable): + """ + Creates a stream ID boundary for a range search. + + Args: + stream_id (TEncodable): The stream ID. + """ + if isinstance(stream_id, bytes): + stream_id = stream_id.decode("utf-8") + self.stream_id = f"{self.EXCLUSIVE_BOUND_VALKEY_API}{stream_id}" + + def to_arg(self) -> TEncodable: + return self.stream_id + + +class StreamReadOptions: + READ_COUNT_VALKEY_API = "COUNT" + READ_BLOCK_VALKEY_API = "BLOCK" + + def __init__(self, block_ms: Optional[int] = None, count: Optional[int] = None): + """ + Options for reading entries from streams. Can be used as an optional argument to `XREAD`. + + Args: + block_ms (Optional[int]): If provided, the request will be blocked for the set amount of milliseconds or + until the server has the required number of entries. Equivalent to `BLOCK` in the Valkey API. + count (Optional[int]): The maximum number of elements requested. Equivalent to `COUNT` in the Valkey API. + """ + self.block_ms = block_ms + self.count = count + + def to_args(self) -> List[TEncodable]: + """ + Returns the options as a list of string arguments to be used in the `XREAD` command. + + Returns: + List[TEncodable]: The options as a list of arguments for the `XREAD` command. + """ + args: List[TEncodable] = [] + if self.block_ms is not None: + args.extend([self.READ_BLOCK_VALKEY_API, str(self.block_ms)]) + + if self.count is not None: + args.extend([self.READ_COUNT_VALKEY_API, str(self.count)]) + + return args + + +class StreamGroupOptions: + MAKE_STREAM_VALKEY_API = "MKSTREAM" + ENTRIES_READ_VALKEY_API = "ENTRIESREAD" + + def __init__(self, make_stream: bool = False, entries_read: Optional[int] = None): + """ + Options for creating stream consumer groups. Can be used as an optional argument to `XGROUP CREATE`. + + Args: + make_stream (bool): If set to True and the stream doesn't exist, this creates a new stream with a + length of 0. + entries_read: (Optional[int]): A value representing the number of stream entries already read by the + group. This option can only be specified if you are using Valkey version 7.0.0 or above. + """ + self.make_stream = make_stream + self.entries_read = entries_read + + def to_args(self) -> List[TEncodable]: + """ + Returns the options as a list of string arguments to be used in the `XGROUP CREATE` command. + + Returns: + List[TEncodable]: The options as a list of arguments for the `XGROUP CREATE` command. + """ + args: List[TEncodable] = [] + if self.make_stream is True: + args.append(self.MAKE_STREAM_VALKEY_API) + + if self.entries_read is not None: + args.extend([self.ENTRIES_READ_VALKEY_API, str(self.entries_read)]) + + return args + + +class StreamReadGroupOptions(StreamReadOptions): + READ_NOACK_VALKEY_API = "NOACK" + + def __init__( + self, no_ack=False, block_ms: Optional[int] = None, count: Optional[int] = None + ): + """ + Options for reading entries from streams using a consumer group. Can be used as an optional argument to + `XREADGROUP`. + + Args: + no_ack (bool): If set, messages are not added to the Pending Entries List (PEL). This is equivalent to + acknowledging the message when it is read. Equivalent to `NOACK` in the Valkey API. + block_ms (Optional[int]): If provided, the request will be blocked for the set amount of milliseconds or + until the server has the required number of entries. Equivalent to `BLOCK` in the Valkey API. + count (Optional[int]): The maximum number of elements requested. Equivalent to `COUNT` in the Valkey API. + """ + super().__init__(block_ms=block_ms, count=count) + self.no_ack = no_ack + + def to_args(self) -> List[TEncodable]: + """ + Returns the options as a list of string arguments to be used in the `XREADGROUP` command. + + Returns: + List[TEncodable]: The options as a list of arguments for the `XREADGROUP` command. + """ + args: List[TEncodable] = super().to_args() + if self.no_ack: + args.append(self.READ_NOACK_VALKEY_API) + + return args + + +class StreamPendingOptions: + IDLE_TIME_VALKEY_API = "IDLE" + + def __init__( + self, + min_idle_time_ms: Optional[int] = None, + consumer_name: Optional[TEncodable] = None, + ): + """ + Options for `XPENDING` that can be used to filter returned items by minimum idle time and consumer name. + + Args: + min_idle_time_ms (Optional[int]): Filters pending entries by their minimum idle time in milliseconds. This + option can only be specified if you are using Valkey version 6.2.0 or above. + consumer_name (Optional[TEncodable]): Filters pending entries by consumer name. + """ + self.min_idle_time = min_idle_time_ms + self.consumer_name = consumer_name + + +class StreamClaimOptions: + IDLE_VALKEY_API = "IDLE" + TIME_VALKEY_API = "TIME" + RETRY_COUNT_VALKEY_API = "RETRYCOUNT" + FORCE_VALKEY_API = "FORCE" + JUST_ID_VALKEY_API = "JUSTID" + + def __init__( + self, + idle: Optional[int] = None, + idle_unix_time: Optional[int] = None, + retry_count: Optional[int] = None, + is_force: Optional[bool] = False, + ): + """ + Options for `XCLAIM`. + + Args: + idle (Optional[int]): Set the idle time (last time it was delivered) of the message in milliseconds. If idle + is not specified, an idle of `0` is assumed, that is, the time count is reset because the message now has a + new owner trying to process it. + idle_unix_time (Optional[int]): This is the same as idle but instead of a relative amount of milliseconds, + it sets the idle time to a specific Unix time (in milliseconds). This is useful in order to rewrite the AOF + file generating `XCLAIM` commands. + retry_count (Optional[int]): Set the retry counter to the specified value. This counter is incremented every + time a message is delivered again. Normally `XCLAIM` does not alter this counter, which is just served to + clients when the `XPENDING` command is called: this way clients can detect anomalies, like messages that + are never processed for some reason after a big number of delivery attempts. + is_force (Optional[bool]): Creates the pending message entry in the PEL even if certain specified IDs are not + already in the PEL assigned to a different client. However, the message must exist in the stream, otherwise + the IDs of non-existing messages are ignored. + """ + self.idle = idle + self.idle_unix_time = idle_unix_time + self.retry_count = retry_count + self.is_force = is_force + + def to_args(self) -> List[TEncodable]: + """ + Converts options for `XCLAIM` into a List. + + Returns: + List[str]: The options as a list of arguments for the `XCLAIM` command. + """ + args: List[TEncodable] = [] + if self.idle: + args.append(self.IDLE_VALKEY_API) + args.append(str(self.idle)) + + if self.idle_unix_time: + args.append(self.TIME_VALKEY_API) + args.append(str(self.idle_unix_time)) + + if self.retry_count: + args.append(self.RETRY_COUNT_VALKEY_API) + args.append(str(self.retry_count)) + + if self.is_force: + args.append(self.FORCE_VALKEY_API) + + return args + + +def _create_xpending_range_args( + key: TEncodable, + group_name: TEncodable, + start: StreamRangeBound, + end: StreamRangeBound, + count: int, + options: Optional[StreamPendingOptions], +) -> List[TEncodable]: + args = [key, group_name] + if options is not None and options.min_idle_time is not None: + args.extend([options.IDLE_TIME_VALKEY_API, str(options.min_idle_time)]) + + args.extend([start.to_arg(), end.to_arg(), str(count)]) + if options is not None and options.consumer_name is not None: + args.append(options.consumer_name) + + return args diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 577a01e4ab..6167b549c1 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -1,27 +1,61 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import threading from typing import List, Mapping, Optional, Tuple, TypeVar, Union +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldSubCommands, + BitmapIndexType, + BitwiseOperation, + OffsetOptions, + _create_bitfield_args, + _create_bitfield_read_only_args, +) +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, + ExpiryGetEx, ExpirySet, + FlushMode, + FunctionRestorePolicy, GeospatialData, + GeoUnit, InfoSection, InsertPosition, UpdateOptions, + _build_sort_args, ) from glide.async_commands.sorted_set import ( + AggregationType, + GeoSearchByBox, + GeoSearchByRadius, + GeoSearchCount, InfBound, LexBoundary, RangeByIndex, RangeByLex, RangeByScore, ScoreBoundary, + ScoreFilter, + _create_geosearch_args, + _create_zinter_zunion_cmd_args, _create_zrange_args, ) -from glide.protobuf.redis_request_pb2 import RequestType +from glide.async_commands.stream import ( + StreamAddOptions, + StreamClaimOptions, + StreamGroupOptions, + StreamPendingOptions, + StreamRangeBound, + StreamReadGroupOptions, + StreamReadOptions, + StreamTrimOptions, + _create_xpending_range_args, +) +from glide.constants import TEncodable +from glide.protobuf.command_request_pb2 import RequestType TTransaction = TypeVar("TTransaction", bound="BaseTransaction") @@ -31,7 +65,7 @@ class BaseTransaction: Base class encompassing shared commands for both standalone and cluster mode implementations in transaction. Command Response: - The response for each command depends on the executed Redis command. Specific response types + The response for each command depends on the executed command. Specific response types are documented alongside each method. Example: @@ -42,11 +76,13 @@ class BaseTransaction: """ def __init__(self) -> None: - self.commands: List[Tuple[RequestType.ValueType, List[str]]] = [] + self.commands: List[Tuple[RequestType.ValueType, List[TEncodable]]] = [] self.lock = threading.Lock() def append_command( - self: TTransaction, request_type: RequestType.ValueType, args: List[str] + self: TTransaction, + request_type: RequestType.ValueType, + args: List[TEncodable], ) -> TTransaction: self.lock.acquire() try: @@ -59,51 +95,88 @@ def clear(self): with self.lock: self.commands.clear() - def get(self: TTransaction, key: str) -> TTransaction: + def get(self: TTransaction, key: TEncodable) -> TTransaction: """ Get the value associated with the given key, or null if no such value exists. - See https://redis.io/commands/get/ for details. + See https://valkey.io/commands/get/ for details. + + Args: + key (TEncodable): The key to retrieve from the database. + + Command response: + Optional[bytes]: If the key exists, returns the value of the key as a bytes string. Otherwise, return None. + """ + return self.append_command(RequestType.Get, [key]) + + def getdel(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Gets a value associated with the given string `key` and deletes the key. + + See https://valkey.io/commands/getdel for more details. Args: - key (str): The key to retrieve from the database. + key (TEncodable): The `key` to retrieve from the database. Command response: - Optional[str]: If the key exists, returns the value of the key as a string. Otherwise, return None. + Optional[bytes]: If `key` exists, returns the `value` of `key`. Otherwise, returns `None`. + """ + return self.append_command(RequestType.GetDel, [key]) + + def getrange( + self: TTransaction, key: TEncodable, start: int, end: int + ) -> TTransaction: + """ + Returns the substring of the string value stored at `key`, determined by the offsets `start` and `end` (both are inclusive). + Negative offsets can be used in order to provide an offset starting from the end of the string. + So `-1` means the last character, `-2` the penultimate and so forth. + + If `key` does not exist, an empty string is returned. If `start` or `end` + are out of range, returns the substring within the valid range of the string. + + See https://valkey.io/commands/getrange/ for more details. + + Args: + key (TEncodable): The key of the string. + start (int): The starting offset. + end (int): The ending offset. + + Commands response: + bytes: A substring extracted from the value stored at `key`. """ - return self.append_command(RequestType.GetString, [key]) + return self.append_command(RequestType.GetRange, [key, str(start), str(end)]) def set( self: TTransaction, - key: str, - value: str, + key: TEncodable, + value: TEncodable, conditional_set: Union[ConditionalChange, None] = None, expiry: Union[ExpirySet, None] = None, return_old_value: bool = False, ) -> TTransaction: """ Set the given key with the given value. Return value is dependent on the passed options. - See https://redis.io/commands/set/ for details. + See https://valkey.io/commands/set/ for details. @example - Set "foo" to "bar" only if "foo" already exists, and set the key expiration to 5 seconds: connection.set("foo", "bar", conditional_set=ConditionalChange.ONLY_IF_EXISTS, expiry=Expiry(ExpiryType.SEC, 5)) Args: - key (str): the key to store. - value (str): the value to store with the given key. + key (TEncodable): the key to store. + value (TEncodable): the value to store with the given key. conditional_set (Optional[ConditionalChange], optional): set the key only if the given condition is met. - Equivalent to [`XX` | `NX`] in the Redis API. Defaults to None. + Equivalent to [`XX` | `NX`] in the Valkey API. Defaults to None. expiry (Optional[ExpirySet], optional): set expiriation to the given key. - Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Redis API. Defaults to None. - return_old_value (bool, optional): Return the old string stored at key, or None if key did not exist. + Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Valkey API. Defaults to None. + return_old_value (bool, optional): Return the old value stored at key, or None if key did not exist. An error is returned and SET aborted if the value stored at key is not a string. - Equivalent to `GET` in the Redis API. Defaults to False. + Equivalent to `GET` in the Valkey API. Defaults to False. Command response: - Optional[str]: + Optional[bytes]: If the value is successfully set, return OK. If value isn't set because of only_if_exists or only_if_does_not_exist conditions, return None. - If return_old_value is set, return the old value as a string. + If return_old_value is set, return the old value as a bytes string. """ args = [key, value] if conditional_set: @@ -115,15 +188,15 @@ def set( args.append("GET") if expiry is not None: args.extend(expiry.get_cmd_args()) - return self.append_command(RequestType.SetString, args) + return self.append_command(RequestType.Set, args) - def strlen(self: TTransaction, key: str) -> TTransaction: + def strlen(self: TTransaction, key: TEncodable) -> TTransaction: """ Get the length of the string value stored at `key`. - See https://redis.io/commands/strlen/ for more details. + See https://valkey.io/commands/strlen/ for more details. Args: - key (str): The key to return its length. + key (TEncodable): The key to return its length. Commands response: int: The length of the string value stored at `key`. @@ -131,33 +204,56 @@ def strlen(self: TTransaction, key: str) -> TTransaction: """ return self.append_command(RequestType.Strlen, [key]) - def rename(self: TTransaction, key: str, new_key: str) -> TTransaction: + def rename( + self: TTransaction, key: TEncodable, new_key: TEncodable + ) -> TTransaction: """ Renames `key` to `new_key`. If `newkey` already exists it is overwritten. In Cluster mode, both `key` and `newkey` must be in the same hash slot, meaning that in practice only keys that have the same hash tag can be reliably renamed in cluster. - See https://redis.io/commands/rename/ for more details. + See https://valkey.io/commands/rename/ for more details. Args: - key (str) : The key to rename. - new_key (str) : The new name of the key. + key (TEncodable) : The key to rename. + new_key (TEncodable) : The new name of the key. Command response: OK: If the `key` was successfully renamed, return "OK". If `key` does not exist, the transaction fails with an error. """ return self.append_command(RequestType.Rename, [key, new_key]) - def custom_command(self: TTransaction, command_args: List[str]) -> TTransaction: + def renamenx( + self: TTransaction, key: TEncodable, new_key: TEncodable + ) -> TTransaction: + """ + Renames `key` to `new_key` if `new_key` does not yet exist. + + See https://valkey.io/commands/renamenx for more details. + + Args: + key (TEncodable): The key to rename. + new_key (TEncodable): The new key name. + + Command response: + bool: True if `key` was renamed to `new_key`, or False if `new_key` already exists. + """ + return self.append_command(RequestType.RenameNX, [key, new_key]) + + def custom_command( + self: TTransaction, command_args: List[TEncodable] + ) -> TTransaction: """ Executes a single command, without checking inputs. - @remarks - This function should only be used for single-response commands. Commands that don't return response (such as SUBSCRIBE), or that return potentially more than a single response (such as XREAD), or that change the client's behavior (such as entering pub/sub mode on RESP2 connections) shouldn't be called using this function. + See the [Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + for details on the restrictions and limitations of the custom command API. + @example - Append a command to list of all pub/sub clients: transaction.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"]) Args: - command_args (List[str]): List of strings of the command's arguments. + command_args (List[TEncodable]): List of command arguments. Every part of the command, including the command name and subcommands, should be added as a separate value in args. Command response: @@ -165,16 +261,16 @@ def custom_command(self: TTransaction, command_args: List[str]) -> TTransaction: """ return self.append_command(RequestType.CustomCommand, command_args) - def append(self: TTransaction, key: str, value: str) -> TTransaction: + def append(self: TTransaction, key: TEncodable, value: TEncodable) -> TTransaction: """ Appends a value to a key. If `key` does not exist it is created and set as an empty string, so `APPEND` will be similar to SET in this special case. - See https://redis.io/commands/append for more details. + See https://valkey.io/commands/append for more details. Args: - key (str): The key to which the value will be appended. - value (str): The value to append. + key (TEncodable): The key to which the value will be appended. + value (TEncodable): The value to append. Commands response: int: The length of the string after appending the value. @@ -186,108 +282,147 @@ def info( sections: Optional[List[InfoSection]] = None, ) -> TTransaction: """ - Get information and statistics about the Redis server. - See https://redis.io/commands/info/ for details. + Get information and statistics about the server. + See https://valkey.io/commands/info/ for details. Args: sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of information to retrieve. When no parameter is provided, the default option is assumed. Command response: - str: Returns a string containing the information for the sections requested. + bytes: Returns a bytes string containing the information for the sections requested. """ - args = [section.value for section in sections] if sections else [] + args: List[TEncodable] = ( + [section.value for section in sections] if sections else [] + ) return self.append_command(RequestType.Info, args) - def delete(self: TTransaction, keys: List[str]) -> TTransaction: + def delete(self: TTransaction, keys: List[TEncodable]) -> TTransaction: """ Delete one or more keys from the database. A key is ignored if it does not exist. - See https://redis.io/commands/del/ for details. + See https://valkey.io/commands/del/ for details. Args: - keys (List[str]): A list of keys to be deleted from the database. + keys (List[TEncodable]): A list of keys to be deleted from the database. Command response: int: The number of keys that were deleted. """ return self.append_command(RequestType.Del, keys) - def config_get(self: TTransaction, parameters: List[str]) -> TTransaction: + def config_get(self: TTransaction, parameters: List[TEncodable]) -> TTransaction: """ Get the values of configuration parameters. - See https://redis.io/commands/config-get/ for details. + See https://valkey.io/commands/config-get/ for details. Args: - parameters (List[str]): A list of configuration parameter names to retrieve values for. + parameters (List[TEncodable]): A list of configuration parameter names to retrieve values for. Command response: - Dict[str, str]: A dictionary of values corresponding to the configuration parameters. + Dict[bytes, bytes]: A dictionary of values corresponding to the configuration parameters. """ return self.append_command(RequestType.ConfigGet, parameters) def config_set( - self: TTransaction, parameters_map: Mapping[str, str] + self: TTransaction, + parameters_map: Mapping[TEncodable, TEncodable], ) -> TTransaction: """ Set configuration parameters to the specified values. - See https://redis.io/commands/config-set/ for details. + See https://valkey.io/commands/config-set/ for details. Args: - parameters_map (Mapping[str, str]): A map consisting of configuration + parameters_map (Mapping[TEncodable, TEncodable]): A map consisting of configuration parameters and their respective values to set. Command response: OK: Returns OK if all configurations have been successfully set. Otherwise, the transaction fails with an error. """ - parameters: List[str] = [] + parameters: List[TEncodable] = [] for pair in parameters_map.items(): parameters.extend(pair) return self.append_command(RequestType.ConfigSet, parameters) def config_resetstat(self: TTransaction) -> TTransaction: """ - Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. - See https://redis.io/commands/config-resetstat/ for details. + Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands. + See https://valkey.io/commands/config-resetstat/ for details. Command response: OK: a simple OK response. """ return self.append_command(RequestType.ConfigResetStat, []) - def mset(self: TTransaction, key_value_map: Mapping[str, str]) -> TTransaction: + def mset( + self: TTransaction, key_value_map: Mapping[TEncodable, TEncodable] + ) -> TTransaction: """ Set multiple keys to multiple values in a single atomic operation. - See https://redis.io/commands/mset/ for more details. + See https://valkey.io/commands/mset/ for more details. Args: - parameters (Mapping[str, str]): A map of key value pairs. + parameters (Mapping[TEncodable, TEncodable]): A map of key value pairs. Command response: OK: a simple OK response. """ - parameters: List[str] = [] + parameters: List[TEncodable] = [] for pair in key_value_map.items(): parameters.extend(pair) return self.append_command(RequestType.MSet, parameters) - def mget(self: TTransaction, keys: List[str]) -> TTransaction: + def msetnx( + self: TTransaction, key_value_map: Mapping[TEncodable, TEncodable] + ) -> TTransaction: + """ + Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + more keys already exist, the entire operation fails. + + See https://valkey.io/commands/msetnx/ for more details. + + Args: + key_value_map (Mapping[TEncodable, TEncodable]): A key-value map consisting of keys and their respective values to set. + + Commands response: + bool: True if all keys were set. False if no key was set. + """ + parameters: List[TEncodable] = [] + for pair in key_value_map.items(): + parameters.extend(pair) + return self.append_command(RequestType.MSetNX, parameters) + + def mget(self: TTransaction, keys: List[TEncodable]) -> TTransaction: """ Retrieve the values of multiple keys. - See https://redis.io/commands/mget/ for more details. + See https://valkey.io/commands/mget/ for more details. Args: - keys (List[str]): A list of keys to retrieve values for. + keys (List[TEncodable]): A list of keys to retrieve values for. Command response: - List[Optional[str]]: A list of values corresponding to the provided keys. If a key is not found, + List[Optional[bytes]]: A list of values corresponding to the provided keys. If a key is not found, its corresponding value in the list will be None. """ return self.append_command(RequestType.MGet, keys) + def touch(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Updates the last access time of specified keys. + + See https://valkey.io/commands/touch/ for details. + + Args: + keys (List[TEncodable]): The keys to update last access time. + + Commands response: + int: The number of keys that were updated, a key is ignored if it doesn't exist. + """ + return self.append_command(RequestType.Touch, keys) + def config_rewrite(self: TTransaction) -> TTransaction: """ Rewrite the configuration file with the current configuration. - See https://redis.io/commands/config-rewrite/ for details. + See https://valkey.io/commands/config-rewrite/ for details. Command response: OK: OK is returned when the configuration was rewritten properly. Otherwise, the transaction fails with an error. @@ -297,36 +432,36 @@ def config_rewrite(self: TTransaction) -> TTransaction: def client_id(self: TTransaction) -> TTransaction: """ Returns the current connection id. - See https://redis.io/commands/client-id/ for more information. + See https://valkey.io/commands/client-id/ for more information. Command response: int: the id of the client. """ return self.append_command(RequestType.ClientId, []) - def incr(self: TTransaction, key: str) -> TTransaction: + def incr(self: TTransaction, key: TEncodable) -> TTransaction: """ Increments the number stored at `key` by one. If `key` does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incr/ for more details. + See https://valkey.io/commands/incr/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. Command response: int: the value of `key` after the increment. """ return self.append_command(RequestType.Incr, [key]) - def incrby(self: TTransaction, key: str, amount: int) -> TTransaction: + def incrby(self: TTransaction, key: TEncodable, amount: int) -> TTransaction: """ Increments the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incrby/ for more details. + See https://valkey.io/commands/incrby/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. amount (int) : The amount to increment. Command response: @@ -334,15 +469,15 @@ def incrby(self: TTransaction, key: str, amount: int) -> TTransaction: """ return self.append_command(RequestType.IncrBy, [key, str(amount)]) - def incrbyfloat(self: TTransaction, key: str, amount: float) -> TTransaction: + def incrbyfloat(self: TTransaction, key: TEncodable, amount: float) -> TTransaction: """ Increment the string representing a floating point number stored at `key` by `amount`. By using a negative increment value, the value stored at the `key` is decremented. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/incrbyfloat/ for more details. + See https://valkey.io/commands/incrbyfloat/ for more details. Args: - key (str): The key to increment its value. + key (TEncodable): The key to increment its value. amount (float) : The amount to increment. Command response: @@ -350,43 +485,43 @@ def incrbyfloat(self: TTransaction, key: str, amount: float) -> TTransaction: """ return self.append_command(RequestType.IncrByFloat, [key, str(amount)]) - def ping(self: TTransaction, message: Optional[str] = None) -> TTransaction: + def ping(self: TTransaction, message: Optional[TEncodable] = None) -> TTransaction: """ - Ping the Redis server. - See https://redis.io/commands/ping/ for more details. + Ping the server. + See https://valkey.io/commands/ping/ for more details. Args: - message (Optional[str]): An optional message to include in the PING command. If not provided, + message (Optional[TEncodable]): An optional message to include in the PING command. If not provided, the server will respond with "PONG". If provided, the server will respond with a copy of the message. Command response: - str: "PONG" if `message` is not provided, otherwise return a copy of `message`. + bytes: b"PONG" if `message` is not provided, otherwise return a copy of `message`. """ argument = [] if message is None else [message] return self.append_command(RequestType.Ping, argument) - def decr(self: TTransaction, key: str) -> TTransaction: + def decr(self: TTransaction, key: TEncodable) -> TTransaction: """ Decrements the number stored at `key` by one. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/decr/ for more details. + See https://valkey.io/commands/decr/ for more details. Args: - key (str): The key to decrement its value. + key (TEncodable): The key to decrement its value. Command response: int: the value of `key` after the decrement. """ return self.append_command(RequestType.Decr, [key]) - def decrby(self: TTransaction, key: str, amount: int) -> TTransaction: + def decrby(self: TTransaction, key: TEncodable, amount: int) -> TTransaction: """ Decrements the number stored at `key` by `amount`. If the key does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/decrby/ for more details. + See https://valkey.io/commands/decrby/ for more details. Args: - key (str): The key to decrement its value. + key (TEncodable): The key to decrement its value. amount (int) : The amount to decrement. Command response: @@ -394,127 +529,160 @@ def decrby(self: TTransaction, key: str, amount: int) -> TTransaction: """ return self.append_command(RequestType.DecrBy, [key, str(amount)]) + def setrange( + self: TTransaction, + key: TEncodable, + offset: int, + value: TEncodable, + ) -> TTransaction: + """ + Overwrites part of the string stored at `key`, starting at the specified + `offset`, for the entire length of `value`. + If the `offset` is larger than the current length of the string at `key`, + the string is padded with zero bytes to make `offset` fit. Creates the `key` + if it doesn't exist. + + See https://valkey.io/commands/setrange for more details. + + Args: + key (TEncodable): The key of the string to update. + offset (int): The position in the string where `value` should be written. + value (TEncodable): The value written with `offset`. + + Command response: + int: The length of the value stored at `key` after it was modified. + """ + return self.append_command(RequestType.SetRange, [key, str(offset), value]) + def hset( - self: TTransaction, key: str, field_value_map: Mapping[str, str] + self: TTransaction, + key: TEncodable, + field_value_map: Mapping[TEncodable, TEncodable], ) -> TTransaction: """ Sets the specified fields to their respective values in the hash stored at `key`. - See https://redis.io/commands/hset/ for more details. + See https://valkey.io/commands/hset/ for more details. Args: - key (str): The key of the hash. - field_value_map (Mapping[str, str]): A field-value map consisting of fields and their corresponding values + key (TEncodable): The key of the hash. + field_value_map (Mapping[TEncodable, TEncodable]): A field-value map consisting of fields and their corresponding values to be set in the hash stored at the specified key. Command response: int: The number of fields that were added to the hash. """ - field_value_list: List[str] = [key] + field_value_list: List[TEncodable] = [key] for pair in field_value_map.items(): field_value_list.extend(pair) - return self.append_command(RequestType.HashSet, field_value_list) + return self.append_command(RequestType.HSet, field_value_list) - def hget(self: TTransaction, key: str, field: str) -> TTransaction: + def hget(self: TTransaction, key: TEncodable, field: TEncodable) -> TTransaction: """ Retrieves the value associated with `field` in the hash stored at `key`. - See https://redis.io/commands/hget/ for more details. + See https://valkey.io/commands/hget/ for more details. Args: - key (str): The key of the hash. - field (str): The field whose value should be retrieved. + key (TEncodable): The key of the hash. + field (TEncodable): The field whose value should be retrieved. Command response: - Optional[str]: The value associated `field` in the hash. + Optional[bytes]: The value associated `field` in the hash. Returns None if `field` is not presented in the hash or `key` does not exist. """ - return self.append_command(RequestType.HashGet, [key, field]) + return self.append_command(RequestType.HGet, [key, field]) def hsetnx( self: TTransaction, - key: str, - field: str, - value: str, + key: TEncodable, + field: TEncodable, + value: TEncodable, ) -> TTransaction: """ Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. If `key` does not exist, a new key holding a hash is created. If `field` already exists, this operation has no effect. - See https://redis.io/commands/hsetnx/ for more details. + See https://valkey.io/commands/hsetnx/ for more details. Args: - key (str): The key of the hash. - field (str): The field to set the value for. - value (str): The value to set. + key (TEncodable): The key of the hash. + field (TEncodable): The field to set the value for. + value (TEncodable): The value to set. Commands response: bool: True if the field was set, False if the field already existed and was not set. """ return self.append_command(RequestType.HSetNX, [key, field, value]) - def hincrby(self: TTransaction, key: str, field: str, amount: int) -> TTransaction: + def hincrby( + self: TTransaction, + key: TEncodable, + field: TEncodable, + amount: int, + ) -> TTransaction: """ Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/hincrby/ for more details. + See https://valkey.io/commands/hincrby/ for more details. Args: - key (str): The key of the hash. - field (str): The field in the hash stored at `key` to increment or decrement its value. + key (TEncodable): The key of the hash. + field (TEncodable): The field in the hash stored at `key` to increment or decrement its value. amount (int): The amount by which to increment or decrement the field's value. Use a negative value to decrement. Command response: int: The value of the specified field in the hash stored at `key` after the increment or decrement. """ - return self.append_command(RequestType.HashIncrBy, [key, field, str(amount)]) + return self.append_command(RequestType.HIncrBy, [key, field, str(amount)]) def hincrbyfloat( - self: TTransaction, key: str, field: str, amount: float + self: TTransaction, + key: TEncodable, + field: TEncodable, + amount: float, ) -> TTransaction: """ Increment or decrement the floating-point value stored at `field` in the hash stored at `key` by the specified amount. By using a negative increment value, the value stored at `field` in the hash stored at `key` is decremented. If `field` or `key` does not exist, it is set to 0 before performing the operation. - See https://redis.io/commands/hincrbyfloat/ for more details. + See https://valkey.io/commands/hincrbyfloat/ for more details. Args: - key (str): The key of the hash. - field (str): The field in the hash stored at `key` to increment or decrement its value. + key (TEncodable): The key of the hash. + field (TEncodable): The field in the hash stored at `key` to increment or decrement its value. amount (float): The amount by which to increment or decrement the field's value. Use a negative value to decrement. Command response: float: The value of the specified field in the hash stored at `key` after the increment as a string. """ - return self.append_command( - RequestType.HashIncrByFloat, [key, field, str(amount)] - ) + return self.append_command(RequestType.HIncrByFloat, [key, field, str(amount)]) - def hexists(self: TTransaction, key: str, field: str) -> TTransaction: + def hexists(self: TTransaction, key: TEncodable, field: TEncodable) -> TTransaction: """ Check if a field exists in the hash stored at `key`. - See https://redis.io/commands/hexists/ for more details. + See https://valkey.io/commands/hexists/ for more details. Args: - key (str): The key of the hash. - field (str): The field to check in the hash stored at `key`. + key (TEncodable): The key of the hash. + field (TEncodable): The field to check in the hash stored at `key`. Command response: bool: Returns 'True' if the hash contains the specified field. If the hash does not contain the field, or if the key does not exist, it returns 'False'. """ - return self.append_command(RequestType.HashExists, [key, field]) + return self.append_command(RequestType.HExists, [key, field]) - def hlen(self: TTransaction, key: str) -> TTransaction: + def hlen(self: TTransaction, key: TEncodable) -> TTransaction: """ Returns the number of fields contained in the hash stored at `key`. - See https://redis.io/commands/hlen/ for more details. + See https://valkey.io/commands/hlen/ for more details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Command response: int: The number of fields in the hash, or 0 when the key does not exist. @@ -525,164 +693,327 @@ def hlen(self: TTransaction, key: str) -> TTransaction: def client_getname(self: TTransaction) -> TTransaction: """ Get the name of the connection on which the transaction is being executed. - See https://redis.io/commands/client-getname/ for more details. + See https://valkey.io/commands/client-getname/ for more details. Command response: - Optional[str]: Returns the name of the client connection as a string if a name is set, + Optional[bytes]: Returns the name of the client connection as a bytes string if a name is set, or None if no name is assigned. """ return self.append_command(RequestType.ClientGetName, []) - def hgetall(self: TTransaction, key: str) -> TTransaction: + def hgetall(self: TTransaction, key: TEncodable) -> TTransaction: """ Returns all fields and values of the hash stored at `key`. - See https://redis.io/commands/hgetall/ for details. + See https://valkey.io/commands/hgetall/ for details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Command response: - Dict[str, str]: A dictionary of fields and their values stored in the hash. Every field name in the list is followed by + Dict[bytes, bytes]: A dictionary of fields and their values stored in the hash. Every field name in the list is followed by its value. If `key` does not exist, it returns an empty dictionary. """ - return self.append_command(RequestType.HashGetAll, [key]) + return self.append_command(RequestType.HGetAll, [key]) - def hmget(self: TTransaction, key: str, fields: List[str]) -> TTransaction: + def hmget( + self: TTransaction, key: TEncodable, fields: List[TEncodable] + ) -> TTransaction: """ Retrieve the values associated with specified fields in the hash stored at `key`. - See https://redis.io/commands/hmget/ for details. + See https://valkey.io/commands/hmget/ for details. Args: - key (str): The key of the hash. - fields (List[str]): The list of fields in the hash stored at `key` to retrieve from the database. + key (TEncodable): The key of the hash. + fields (List[TEncodable]): The list of fields in the hash stored at `key` to retrieve from the database. Returns: - List[Optional[str]]: A list of values associated with the given fields, in the same order as they are requested. + List[Optional[bytes]]: A list of values associated with the given fields, in the same order as they are requested. For every field that does not exist in the hash, a null value is returned. If `key` does not exist, it is treated as an empty hash, and the function returns a list of null values. """ - return self.append_command(RequestType.HashMGet, [key] + fields) + return self.append_command(RequestType.HMGet, [key] + fields) - def hdel(self: TTransaction, key: str, fields: List[str]) -> TTransaction: + def hdel( + self: TTransaction, key: TEncodable, fields: List[TEncodable] + ) -> TTransaction: """ Remove specified fields from the hash stored at `key`. - See https://redis.io/commands/hdel/ for more details. + See https://valkey.io/commands/hdel/ for more details. Args: - key (str): The key of the hash. - fields (List[str]): The list of fields to remove from the hash stored at `key`. + key (TEncodable): The key of the hash. + fields (List[TEncodable]): The list of fields to remove from the hash stored at `key`. Returns: int: The number of fields that were removed from the hash, excluding specified but non-existing fields. If `key` does not exist, it is treated as an empty hash, and the function returns 0. """ - return self.append_command(RequestType.HashDel, [key] + fields) + return self.append_command(RequestType.HDel, [key] + fields) - def hvals(self: TTransaction, key: str) -> TTransaction: + def hvals(self: TTransaction, key: TEncodable) -> TTransaction: """ Returns all values in the hash stored at `key`. - See https://redis.io/commands/hvals/ for more details. + See https://valkey.io/commands/hvals/ for more details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. Command response: - List[str]: A list of values in the hash, or an empty list when the key does not exist. + List[bytes]: A list of values in the hash, or an empty list when the key does not exist. """ - return self.append_command(RequestType.Hvals, [key]) + return self.append_command(RequestType.HVals, [key]) - def hkeys(self: TTransaction, key: str) -> TTransaction: + def hkeys(self: TTransaction, key: TEncodable) -> TTransaction: """ Returns all field names in the hash stored at `key`. - See https://redis.io/commands/hkeys/ for more details. + See https://valkey.io/commands/hkeys/ for more details. + + Args: + key (TEncodable): The key of the hash. + + Command response: + List[bytes]: A list of field names for the hash, or an empty list when the key does not exist. + """ + return self.append_command(RequestType.HKeys, [key]) + + def hrandfield(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns a random field name from the hash value stored at `key`. + + See https://valkey.io/commands/hrandfield for more details. + + Args: + key (TEncodable): The key of the hash. + + Command response: + Optional[bytes]: A random field name from the hash stored at `key`. + If the hash does not exist or is empty, None will be returned. + """ + return self.append_command(RequestType.HRandField, [key]) + + def hrandfield_count( + self: TTransaction, key: TEncodable, count: int + ) -> TTransaction: + """ + Retrieves up to `count` random field names from the hash value stored at `key`. + + See https://valkey.io/commands/hrandfield for more details. + + Args: + key (TEncodable): The key of the hash. + count (int): The number of field names to return. + If `count` is positive, returns unique elements. + If `count` is negative, allows for duplicates elements. + + Command response: + List[bytes]: A list of random field names from the hash. + If the hash does not exist or is empty, the response will be an empty list. + """ + return self.append_command(RequestType.HRandField, [key, str(count)]) + + def hrandfield_withvalues( + self: TTransaction, key: TEncodable, count: int + ) -> TTransaction: + """ + Retrieves up to `count` random field names along with their values from the hash value stored at `key`. + + See https://valkey.io/commands/hrandfield for more details. Args: - key (str): The key of the hash. + key (TEncodable): The key of the hash. + count (int): The number of field names to return. + If `count` is positive, returns unique elements. + If `count` is negative, allows for duplicates elements. Command response: - List[str]: A list of field names for the hash, or an empty list when the key does not exist. + List[List[bytes]]: A list of `[field_name, value]` lists, where `field_name` is a random field name from the + hash and `value` is the associated value of the field name. + If the hash does not exist or is empty, the response will be an empty list. + """ + return self.append_command( + RequestType.HRandField, [key, str(count), "WITHVALUES"] + ) + + def hstrlen(self: TTransaction, key: TEncodable, field: TEncodable) -> TTransaction: + """ + Returns the string length of the value associated with `field` in the hash stored at `key`. + + See https://valkey.io/commands/hstrlen/ for more details. + + Args: + key (TEncodable): The key of the hash. + field (TEncodable): The field in the hash. + + Commands response: + int: The string length or 0 if `field` or `key` does not exist. """ - return self.append_command(RequestType.Hkeys, [key]) + return self.append_command(RequestType.HStrlen, [key, field]) - def lpush(self: TTransaction, key: str, elements: List[str]) -> TTransaction: + def lpush( + self: TTransaction, key: TEncodable, elements: List[TEncodable] + ) -> TTransaction: """ Insert all the specified values at the head of the list stored at `key`. `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. - See https://redis.io/commands/lpush/ for more details. + See https://valkey.io/commands/lpush/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the head of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the head of the list stored at `key`. Command response: int: The length of the list after the push operations. """ return self.append_command(RequestType.LPush, [key] + elements) - def lpushx(self: TTransaction, key: str, elements: List[str]) -> TTransaction: + def lpushx( + self: TTransaction, key: TEncodable, elements: List[TEncodable] + ) -> TTransaction: """ - Inserts specified values at the head of the `list`, only if `key` already exists and holds a list. + Inserts all the specified values at the head of the list stored at `key`, only if `key` exists and holds a list. + If `key` is not a list, this performs no operation. - See https://redis.io/commands/lpushx/ for more details. + See https://valkey.io/commands/lpushx/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the head of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the head of the list stored at `key`. Command response: int: The length of the list after the push operation. """ return self.append_command(RequestType.LPushX, [key] + elements) - def lpop(self: TTransaction, key: str) -> TTransaction: + def lpop(self: TTransaction, key: TEncodable) -> TTransaction: """ Remove and return the first elements of the list stored at `key`. The command pops a single element from the beginning of the list. - See https://redis.io/commands/lpop/ for details. + See https://valkey.io/commands/lpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. Command response: - Optional[str]: The value of the first element. + Optional[bytes]: The value of the first element. If `key` does not exist, None will be returned. """ return self.append_command(RequestType.LPop, [key]) - def lpop_count(self: TTransaction, key: str, count: int) -> TTransaction: + def lpop_count(self: TTransaction, key: TEncodable, count: int) -> TTransaction: """ Remove and return up to `count` elements from the list stored at `key`, depending on the list's length. - See https://redis.io/commands/lpop/ for details. + See https://valkey.io/commands/lpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. count (int): The count of elements to pop from the list. Command response: - Optional[List[str]]: A a list of popped elements will be returned depending on the list's length. + Optional[List[bytes]]: A a list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. """ return self.append_command(RequestType.LPop, [key, str(count)]) - def lrange(self: TTransaction, key: str, start: int, end: int) -> TTransaction: + def blpop( + self: TTransaction, keys: List[TEncodable], timeout: float + ) -> TTransaction: + """ + Pops an element from the head of the first list that is non-empty, with the given keys being checked in the + order that they are given. Blocks the connection when there are no elements to pop from any of the given lists. + + See https://valkey.io/commands/blpop for details. + + BLPOP is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + Args: + keys (List[TEncodable]): The keys of the lists to pop from. + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. + + Command response: + Optional[List[bytes]]: A two-element list containing the key from which the element was popped and the value of the + popped element, formatted as `[key, value]`. If no element could be popped and the `timeout` expired, returns None. + """ + return self.append_command(RequestType.BLPop, keys + [str(timeout)]) + + def lmpop( + self: TTransaction, + keys: List[TEncodable], + direction: ListDirection, + count: Optional[int] = None, + ) -> TTransaction: + """ + Pops one or more elements from the first non-empty list from the provided `keys`. + + See https://valkey.io/commands/lmpop/ for details. + + Args: + keys (List[TEncodable]): An array of keys of lists. + direction (ListDirection): The direction based on which elements are popped from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + count (Optional[int]): The maximum number of popped elements. If not provided, defaults to popping a single element. + + Command response: + Optional[Mapping[bytes, List[bytes]]]: A map of `key` name mapped to an array of popped elements, or None if no elements could be popped. + + Since: Valkey version 7.0.0. + """ + args = [str(len(keys)), *keys, direction.value] + if count is not None: + args += ["COUNT", str(count)] + + return self.append_command(RequestType.LMPop, args) + + def blmpop( + self: TTransaction, + keys: List[TEncodable], + direction: ListDirection, + timeout: float, + count: Optional[int] = None, + ) -> TTransaction: + """ + Blocks the connection until it pops one or more elements from the first non-empty list from the provided `keys`. + + `BLMPOP` is the blocking variant of `LMPOP`. + + See https://valkey.io/commands/blmpop/ for details. + + Args: + keys (List[TEncodable]): An array of keys of lists. + direction (ListDirection): The direction based on which elements are popped from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + count (Optional[int]): The maximum number of popped elements. If not provided, defaults to popping a single element. + + Command response: + Optional[Mapping[bytes, List[bytes]]]: A map of `key` name mapped to an array of popped elements, or None if no elements could be popped and the timeout expired. + + Since: Valkey version 7.0.0. + """ + args = [str(timeout), str(len(keys)), *keys, direction.value] + if count is not None: + args += ["COUNT", str(count)] + + return self.append_command(RequestType.BLMPop, args) + + def lrange( + self: TTransaction, key: TEncodable, start: int, end: int + ) -> TTransaction: """ Retrieve the specified elements of the list stored at `key` within the given range. The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next element and so on. These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last element of the list, -2 being the penultimate, and so on. - See https://redis.io/commands/lrange/ for details. + See https://valkey.io/commands/lrange/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. start (int): The starting point of the range. end (int): The end of the range. Command response: - List[str]: A list of elements within the specified range. + List[byte]: A list of elements within the specified range. If `start` exceeds the `end` of the list, or if `start` is greater than `end`, an empty list will be returned. If `end` exceeds the actual end of the list, the range will stop at the actual end of the list. If `key` does not exist an empty list will be returned. @@ -691,7 +1022,7 @@ def lrange(self: TTransaction, key: str, start: int, end: int) -> TTransaction: def lindex( self: TTransaction, - key: str, + key: TEncodable, index: int, ) -> TTransaction: """ @@ -701,27 +1032,55 @@ def lindex( Negative indices can be used to designate elements starting at the tail of the list. Here, -1 means the last element, -2 means the penultimate and so forth. - See https://redis.io/commands/lindex/ for more details. + See https://valkey.io/commands/lindex/ for more details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. index (int): The index of the element in the list to retrieve. Command response: - Optional[str]: The element at `index` in the list stored at `key`. + Optional[bytes]: The element at `index` in the list stored at `key`. If `index` is out of range or if `key` does not exist, None is returned. """ - return self.append_command(RequestType.Lindex, [key, str(index)]) + return self.append_command(RequestType.LIndex, [key, str(index)]) + + def lset( + self: TTransaction, + key: TEncodable, + index: int, + element: TEncodable, + ) -> TTransaction: + """ + Sets the list element at `index` to `element`. + + The index is zero-based, so `0` means the first element, `1` the second element and so on. + Negative indices can be used to designate elements starting at the tail of the list. + Here, `-1` means the last element, `-2` means the penultimate and so forth. + + See https://valkey.io/commands/lset/ for details. + + Args: + key (TEncodable): The key of the list. + index (int): The index of the element in the list to be set. + element (TEncodable): The new element to set at the specified index. + + Commands response: + TOK: A simple `OK` response. + """ + return self.append_command(RequestType.LSet, [key, str(index), element]) - def rpush(self: TTransaction, key: str, elements: List[str]) -> TTransaction: - """Inserts all the specified values at the tail of the list stored at `key`. + def rpush( + self: TTransaction, key: TEncodable, elements: List[TEncodable] + ) -> TTransaction: + """ + Inserts all the specified values at the tail of the list stored at `key`. `elements` are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. If `key` does not exist, it is created as empty list before performing the push operations. - See https://redis.io/commands/rpush/ for more details. + See https://valkey.io/commands/rpush/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the tail of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the tail of the list stored at `key`. Command response: int: The length of the list after the push operations. @@ -729,65 +1088,95 @@ def rpush(self: TTransaction, key: str, elements: List[str]) -> TTransaction: """ return self.append_command(RequestType.RPush, [key] + elements) - def rpushx(self: TTransaction, key: str, elements: List[str]) -> TTransaction: + def rpushx( + self: TTransaction, key: TEncodable, elements: List[TEncodable] + ) -> TTransaction: """ - Inserts specified values at the tail of the `list`, only if `key` already exists and holds a list. + Inserts all the specified values at the tail of the list stored at `key`, only if `key` exists and holds a list. + If `key` is not a list, this performs no operation. - See https://redis.io/commands/rpushx/ for more details. + See https://valkey.io/commands/rpushx/ for more details. Args: - key (str): The key of the list. - elements (List[str]): The elements to insert at the tail of the list stored at `key`. + key (TEncodable): The key of the list. + elements (List[TEncodable]): The elements to insert at the tail of the list stored at `key`. Command response: int: The length of the list after the push operation. """ return self.append_command(RequestType.RPushX, [key] + elements) - def rpop(self: TTransaction, key: str, count: Optional[int] = None) -> TTransaction: + def rpop( + self: TTransaction, key: TEncodable, count: Optional[int] = None + ) -> TTransaction: """ Removes and returns the last elements of the list stored at `key`. The command pops a single element from the end of the list. - See https://redis.io/commands/rpop/ for details. + See https://valkey.io/commands/rpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. Commands response: - Optional[str]: The value of the last element. + Optional[bytes]: The value of the last element. If `key` does not exist, None will be returned. """ return self.append_command(RequestType.RPop, [key]) - def rpop_count(self: TTransaction, key: str, count: int) -> TTransaction: + def rpop_count(self: TTransaction, key: TEncodable, count: int) -> TTransaction: """ Removes and returns up to `count` elements from the list stored at `key`, depending on the list's length. - See https://redis.io/commands/rpop/ for details. + See https://valkey.io/commands/rpop/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. count (int): The count of elements to pop from the list. Commands response: - Optional[List[str]: A list of popped elements will be returned depending on the list's length. + Optional[List[bytes]: A list of popped elements will be returned depending on the list's length. If `key` does not exist, None will be returned. """ return self.append_command(RequestType.RPop, [key, str(count)]) + def brpop( + self: TTransaction, keys: List[TEncodable], timeout: float + ) -> TTransaction: + """ + Pops an element from the tail of the first list that is non-empty, with the given keys being checked in the + order that they are given. Blocks the connection when there are no elements to pop from any of the given lists. + + See https://valkey.io/commands/brpop for details. + + BRPOP is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + Args: + keys (List[TEncodable]): The keys of the lists to pop from. + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. + + Command response: + Optional[List[bytes]]: A two-element list containing the key from which the element was popped and the value of the + popped element, formatted as `[key, value]`. If no element could be popped and the `timeout` expired, returns None. + """ + return self.append_command(RequestType.BRPop, keys + [str(timeout)]) + def linsert( - self: TTransaction, key: str, position: InsertPosition, pivot: str, element: str + self: TTransaction, + key: TEncodable, + position: InsertPosition, + pivot: TEncodable, + element: TEncodable, ) -> TTransaction: """ Inserts `element` in the list at `key` either before or after the `pivot`. - See https://redis.io/commands/linsert/ for details. + See https://valkey.io/commands/linsert/ for details. Args: - key (str): The key of the list. + key (TEncodable): The key of the list. position (InsertPosition): The relative position to insert into - either `InsertPosition.BEFORE` or `InsertPosition.AFTER` the `pivot`. - pivot (str): An element of the list. - element (str): The new element to insert. + pivot (TEncodable): An element of the list. + element (TEncodable): The new element to insert. Command response: int: The list length after a successful insert operation. @@ -798,31 +1187,97 @@ def linsert( RequestType.LInsert, [key, position.value, pivot, element] ) - def sadd(self: TTransaction, key: str, members: List[str]) -> TTransaction: + def lmove( + self: TTransaction, + source: TEncodable, + destination: TEncodable, + where_from: ListDirection, + where_to: ListDirection, + ) -> TTransaction: + """ + Atomically pops and removes the left/right-most element to the list stored at `source` + depending on `where_from`, and pushes the element at the first/last element of the list + stored at `destination` depending on `where_to`. + + See https://valkey.io/commands/lmove/ for details. + + Args: + source (TEncodable): The key to the source list. + destination (TEncodable): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + + Command response: + Optional[bytes]: The popped element, or `None` if `source` does not exist. + + Since: Valkey version 6.2.0. + """ + return self.append_command( + RequestType.LMove, [source, destination, where_from.value, where_to.value] + ) + + def blmove( + self: TTransaction, + source: TEncodable, + destination: TEncodable, + where_from: ListDirection, + where_to: ListDirection, + timeout: float, + ) -> TTransaction: + """ + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `blmove` is the blocking variant of `lmove`. + + See https://valkey.io/commands/blmove/ for details. + + Args: + source (TEncodable): The key to the source list. + destination (TEncodable): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + + Command response: + Optional[bytes]: The popped element, or `None` if `source` does not exist or if the operation timed-out. + + Since: Valkey version 6.2.0. + """ + return self.append_command( + RequestType.BLMove, + [source, destination, where_from.value, where_to.value, str(timeout)], + ) + + def sadd( + self: TTransaction, key: TEncodable, members: List[TEncodable] + ) -> TTransaction: """ Add specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. If `key` does not exist, a new set is created before adding `members`. - See https://redis.io/commands/sadd/ for more details. + See https://valkey.io/commands/sadd/ for more details. Args: - key (str): The key where members will be added to its set. - members (List[str]): A list of members to add to the set stored at key. + key (TEncodable): The key where members will be added to its set. + members (List[TEncodable]): A list of members to add to the set stored at key. Command response: int: The number of members that were added to the set, excluding members already present. """ return self.append_command(RequestType.SAdd, [key] + members) - def srem(self: TTransaction, key: str, members: List[str]) -> TTransaction: + def srem( + self: TTransaction, key: TEncodable, members: List[TEncodable] + ) -> TTransaction: """ Remove specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. - See https://redis.io/commands/srem/ for details. + See https://valkey.io/commands/srem/ for details. Args: - key (str): The key from which members will be removed. - members (List[str]): A list of members to remove from the set stored at key. + key (TEncodable): The key from which members will be removed. + members (List[TEncodable]): A list of members to remove from the set stored at key. Commands response: int: The number of members that were removed from the set, excluding non-existing members. @@ -830,34 +1285,34 @@ def srem(self: TTransaction, key: str, members: List[str]) -> TTransaction: """ return self.append_command(RequestType.SRem, [key] + members) - def smembers(self: TTransaction, key: str) -> TTransaction: + def smembers(self: TTransaction, key: TEncodable) -> TTransaction: """ Retrieve all the members of the set value stored at `key`. - See https://redis.io/commands/smembers/ for details. + See https://valkey.io/commands/smembers/ for details. Args: - key (str): The key from which to retrieve the set members. + key (TEncodable): The key from which to retrieve the set members. Commands response: - Set[str]: A set of all members of the set. + Set[bytes]: A set of all members of the set. If `key` does not exist an empty list will be returned. """ return self.append_command(RequestType.SMembers, [key]) - def scard(self: TTransaction, key: str) -> TTransaction: + def scard(self: TTransaction, key: TEncodable) -> TTransaction: """ Retrieve the set cardinality (number of elements) of the set stored at `key`. - See https://redis.io/commands/scard/ for details. + See https://valkey.io/commands/scard/ for details. Args: - key (str): The key from which to retrieve the number of set members. + key (TEncodable): The key from which to retrieve the number of set members. Commands response: int: The cardinality (number of elements) of the set, or 0 if the key does not exist. """ return self.append_command(RequestType.SCard, [key]) - def spop(self: TTransaction, key: str) -> TTransaction: + def spop(self: TTransaction, key: TEncodable) -> TTransaction: """ Removes and returns one random member from the set stored at `key`. @@ -865,15 +1320,15 @@ def spop(self: TTransaction, key: str) -> TTransaction: To pop multiple members, see `spop_count`. Args: - key (str): The key of the set. + key (TEncodable): The key of the set. Commands response: - Optional[str]: The value of the popped member. + Optional[bytes]: The value of the popped member. If `key` does not exist, None will be returned. """ - return self.append_command(RequestType.Spop, [key]) + return self.append_command(RequestType.SPop, [key]) - def spop_count(self: TTransaction, key: str, count: int) -> TTransaction: + def spop_count(self: TTransaction, key: TEncodable, count: int) -> TTransaction: """ Removes and returns up to `count` random members from the set stored at `key`, depending on the set's length. @@ -881,28 +1336,28 @@ def spop_count(self: TTransaction, key: str, count: int) -> TTransaction: To pop a single member, see `spop`. Args: - key (str): The key of the set. + key (TEncodable): The key of the set. count (int): The count of the elements to pop from the set. Commands response: - Set[str]: A set of popped elements will be returned depending on the set's length. + Set[bytes]: A set of popped elements will be returned depending on the set's length. If `key` does not exist, an empty set will be returned. """ - return self.append_command(RequestType.Spop, [key, str(count)]) + return self.append_command(RequestType.SPop, [key, str(count)]) def sismember( self: TTransaction, - key: str, - member: str, + key: TEncodable, + member: TEncodable, ) -> TTransaction: """ Returns if `member` is a member of the set stored at `key`. - See https://redis.io/commands/sismember/ for more details. + See https://valkey.io/commands/sismember/ for more details. Args: - key (str): The key of the set. - member (str): The member to check for existence in the set. + key (TEncodable): The key of the set. + member (TEncodable): The member to check for existence in the set. Commands response: bool: True if the member exists in the set, False otherwise. @@ -910,96 +1365,268 @@ def sismember( """ return self.append_command(RequestType.SIsMember, [key, member]) - def ltrim(self: TTransaction, key: str, start: int, end: int) -> TTransaction: + def smove( + self: TTransaction, + source: TEncodable, + destination: TEncodable, + member: TEncodable, + ) -> TTransaction: """ - Trim an existing list so that it will contain only the specified range of elements specified. - The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next - element and so on. - These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last - element of the list, -2 being the penultimate, and so on. - See https://redis.io/commands/ltrim/ for more details. + Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. Creates a + new destination set if needed. The operation is atomic. + + See https://valkey.io/commands/smove for more details. Args: - key (str): The key of the list. - start (int): The starting point of the range. - end (int): The end of the range. + source (TEncodable): The key of the set to remove the element from. + destination (TEncodable): The key of the set to add the element to. + member (TEncodable): The set element to move. - Commands response: - TOK: A simple "OK" response. - If `start` exceeds the end of the list, or if `start` is greater than `end`, the result will be an empty list - (which causes `key` to be removed). - If `end` exceeds the actual end of the list, it will be treated like the last element of the list. - f `key` does not exist, the response will be "OK" without changes to the database. + Command response: + bool: True on success, or False if the `source` set does not exist or the element is not a member of the source set. """ - return self.append_command(RequestType.LTrim, [key, str(start), str(end)]) + return self.append_command(RequestType.SMove, [source, destination, member]) - def lrem(self: TTransaction, key: str, count: int, element: str) -> TTransaction: + def sunion(self: TTransaction, keys: List[TEncodable]) -> TTransaction: """ - Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. - If `count` is positive, it removes elements equal to `element` moving from head to tail. - If `count` is negative, it removes elements equal to `element` moving from tail to head. - If `count` is 0 or greater than the occurrences of elements equal to `element`, it removes all elements - equal to `element`. - See https://redis.io/commands/lrem/ for more details. + Gets the union of all the given sets. + + See https://valkey.io/commands/sunion for more details. Args: - key (str): The key of the list. - count (int): The count of occurrences of elements equal to `element` to remove. - element (str): The element to remove from the list. + keys (List[TEncodable]): The keys of the sets. Commands response: - int: The number of removed elements. - If `key` does not exist, 0 is returned. + Set[bytes]: A set of members which are present in at least one of the given sets. + If none of the sets exist, an empty set will be returned. """ - return self.append_command(RequestType.LRem, [key, str(count), element]) + return self.append_command(RequestType.SUnion, keys) - def llen(self: TTransaction, key: str) -> TTransaction: + def sunionstore( + self: TTransaction, + destination: TEncodable, + keys: List[TEncodable], + ) -> TTransaction: """ - Get the length of the list stored at `key`. - See https://redis.io/commands/llen/ for details. + Stores the members of the union of all given sets specified by `keys` into a new set at `destination`. + + See https://valkey.io/commands/sunionstore for more details. Args: - key (str): The key of the list. + destination (TEncodable): The key of the destination set. + keys (List[TEncodable]): The keys from which to retrieve the set members. - Commands response: - int: The length of the list at the specified key. - If `key` does not exist, it is interpreted as an empty list and 0 is returned. + Command response: + int: The number of elements in the resulting set. """ - return self.append_command(RequestType.LLen, [key]) + return self.append_command(RequestType.SUnionStore, [destination] + keys) - def exists(self: TTransaction, keys: List[str]) -> TTransaction: + def sinter(self: TTransaction, keys: List[TEncodable]) -> TTransaction: """ - Returns the number of keys in `keys` that exist in the database. - See https://redis.io/commands/exists/ for more details. + Gets the intersection of all the given sets. + + See https://valkey.io/commands/sinter for more details. Args: - keys (List[str]): The list of keys to check. + keys (List[TEncodable]): The keys of the sets. - Commands response: - int: The number of keys that exist. If the same existing key is mentioned in `keys` multiple times, - it will be counted multiple times. + Command response: + Set[bytes]: A set of members which are present in all given sets. + If one or more sets do not exist, an empty set will be returned. """ - return self.append_command(RequestType.Exists, keys) + return self.append_command(RequestType.SInter, keys) - def unlink(self: TTransaction, keys: List[str]) -> TTransaction: + def sinterstore( + self: TTransaction, + destination: TEncodable, + keys: List[TEncodable], + ) -> TTransaction: """ - Unlink (delete) multiple keys from the database. - A key is ignored if it does not exist. - This command, similar to DEL, removes specified keys and ignores non-existent ones. - However, this command does not block the server, while [DEL](https://redis.io/commands/del) does. - See https://redis.io/commands/unlink/ for more details. + Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. + + See https://valkey.io/commands/sinterstore for more details. Args: - keys (List[str]): The list of keys to unlink. + destination (TEncodable): The key of the destination set. + keys (List[TEncodable]): The keys from which to retrieve the set members. - Commands response: - int: The number of keys that were unlinked. + Command response: + int: The number of elements in the resulting set. + """ + return self.append_command(RequestType.SInterStore, [destination] + keys) + + def sintercard( + self: TTransaction, keys: List[TEncodable], limit: Optional[int] = None + ) -> TTransaction: + """ + Gets the cardinality of the intersection of all the given sets. + Optionally, a `limit` can be specified to stop the computation early if the intersection cardinality reaches the specified limit. + + See https://valkey.io/commands/sintercard for more details. + + Args: + keys (List[TEncodable]): A list of keys representing the sets to intersect. + limit (Optional[int]): An optional limit to the maximum number of intersecting elements to count. + If specified, the computation stops as soon as the cardinality reaches this limit. + + Command response: + int: The number of elements in the resulting set of the intersection. + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + if limit is not None: + args.extend(["LIMIT", str(limit)]) + return self.append_command(RequestType.SInterCard, args) + + def sdiff(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Computes the difference between the first set and all the successive sets in `keys`. + + See https://valkey.io/commands/sdiff for more details. + + Args: + keys (List[TEncodable]): The keys of the sets to diff. + + Command response: + Set[bytes]: A set of elements representing the difference between the sets. + If any of the keys in `keys` do not exist, they are treated as empty sets. + """ + return self.append_command(RequestType.SDiff, keys) + + def sdiffstore( + self: TTransaction, + destination: TEncodable, + keys: List[TEncodable], + ) -> TTransaction: + """ + Stores the difference between the first set and all the successive sets in `keys` into a new set at + `destination`. + + See https://valkey.io/commands/sdiffstore for more details. + + Args: + destination (TEncodable): The key of the destination set. + keys (List[TEncodable]): The keys of the sets to diff. + + Command response: + int: The number of elements in the resulting set. + """ + return self.append_command(RequestType.SDiffStore, [destination] + keys) + + def smismember( + self: TTransaction, key: TEncodable, members: List[TEncodable] + ) -> TTransaction: + """ + Checks whether each member is contained in the members of the set stored at `key`. + + See https://valkey.io/commands/smismember for more details. + + Args: + key (TEncodable): The key of the set to check. + members (List[TEncodable]): A list of members to check for existence in the set. + + Command response: + List[bool]: A list of bool values, each indicating if the respective member exists in the set. + """ + return self.append_command(RequestType.SMIsMember, [key] + members) + + def ltrim( + self: TTransaction, key: TEncodable, start: int, end: int + ) -> TTransaction: + """ + Trim an existing list so that it will contain only the specified range of elements specified. + The offsets `start` and `end` are zero-based indexes, with 0 being the first element of the list, 1 being the next + element and so on. + These offsets can also be negative numbers indicating offsets starting at the end of the list, with -1 being the last + element of the list, -2 being the penultimate, and so on. + See https://valkey.io/commands/ltrim/ for more details. + + Args: + key (TEncodable): The key of the list. + start (int): The starting point of the range. + end (int): The end of the range. + + Commands response: + TOK: A simple "OK" response. + If `start` exceeds the end of the list, or if `start` is greater than `end`, the result will be an empty list + (which causes `key` to be removed). + If `end` exceeds the actual end of the list, it will be treated like the last element of the list. + f `key` does not exist, the response will be "OK" without changes to the database. + """ + return self.append_command(RequestType.LTrim, [key, str(start), str(end)]) + + def lrem( + self: TTransaction, + key: TEncodable, + count: int, + element: TEncodable, + ) -> TTransaction: + """ + Removes the first `count` occurrences of elements equal to `element` from the list stored at `key`. + If `count` is positive, it removes elements equal to `element` moving from head to tail. + If `count` is negative, it removes elements equal to `element` moving from tail to head. + If `count` is 0 or greater than the occurrences of elements equal to `element`, it removes all elements + equal to `element`. + See https://valkey.io/commands/lrem/ for more details. + + Args: + key (TEncodable): The key of the list. + count (int): The count of occurrences of elements equal to `element` to remove. + element (TEncodable): The element to remove from the list. + + Commands response: + int: The number of removed elements. + If `key` does not exist, 0 is returned. + """ + return self.append_command(RequestType.LRem, [key, str(count), element]) + + def llen(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Get the length of the list stored at `key`. + See https://valkey.io/commands/llen/ for details. + + Args: + key (TEncodable): The key of the list. + + Commands response: + int: The length of the list at the specified key. + If `key` does not exist, it is interpreted as an empty list and 0 is returned. + """ + return self.append_command(RequestType.LLen, [key]) + + def exists(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Returns the number of keys in `keys` that exist in the database. + See https://valkey.io/commands/exists/ for more details. + + Args: + keys (List[TEncodable]): The list of keys to check. + + Commands response: + int: The number of keys that exist. If the same existing key is mentioned in `keys` multiple times, + it will be counted multiple times. + """ + return self.append_command(RequestType.Exists, keys) + + def unlink(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Unlink (delete) multiple keys from the database. + A key is ignored if it does not exist. + This command, similar to DEL, removes specified keys and ignores non-existent ones. + However, this command does not block the server, while [DEL](https://valkey.io/commands/del) does. + See https://valkey.io/commands/unlink/ for more details. + + Args: + keys (List[TEncodable]): The list of keys to unlink. + + Commands response: + int: The number of keys that were unlinked. """ return self.append_command(RequestType.Unlink, keys) def expire( self: TTransaction, - key: str, + key: TEncodable, seconds: int, option: Optional[ExpireOptions] = None, ) -> TTransaction: @@ -1008,10 +1635,10 @@ def expire( If `key` already has an existing expire set, the time to live is updated to the new value. If `seconds` is a non-positive number, the key will be deleted rather than expired. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/expire/ for more details. + See https://valkey.io/commands/expire/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. seconds (int): The timeout in seconds. option (Optional[ExpireOptions]): The expire option. @@ -1019,14 +1646,14 @@ def expire( bool: 'True' if the timeout was set, 'False' if the timeout was not set (e.g., the key doesn't exist or the operation is skipped due to the provided arguments). """ - args: List[str] = ( + args: List[TEncodable] = ( [key, str(seconds)] if option is None else [key, str(seconds), option.value] ) return self.append_command(RequestType.Expire, args) def expireat( self: TTransaction, - key: str, + key: TEncodable, unix_seconds: int, option: Optional[ExpireOptions] = None, ) -> TTransaction: @@ -1037,10 +1664,10 @@ def expireat( deleted. If `key` already has an existing expire set, the time to live is updated to the new value. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/expireat/ for more details. + See https://valkey.io/commands/expireat/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. unix_seconds (int): The timeout in an absolute Unix timestamp. option (Optional[ExpireOptions]): The expire option. @@ -1057,7 +1684,7 @@ def expireat( def pexpire( self: TTransaction, - key: str, + key: TEncodable, milliseconds: int, option: Optional[ExpireOptions] = None, ) -> TTransaction: @@ -1066,10 +1693,10 @@ def pexpire( If `key` already has an existing expire set, the time to live is updated to the new value. If `milliseconds` is a non-positive number, the key will be deleted rather than expired. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/pexpire/ for more details. + See https://valkey.io/commands/pexpire/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. milliseconds (int): The timeout in milliseconds. option (Optional[ExpireOptions]): The expire option. @@ -1086,7 +1713,7 @@ def pexpire( def pexpireat( self: TTransaction, - key: str, + key: TEncodable, unix_milliseconds: int, option: Optional[ExpireOptions] = None, ) -> TTransaction: @@ -1097,10 +1724,10 @@ def pexpireat( deleted. If `key` already has an existing expire set, the time to live is updated to the new value. The timeout will only be cleared by commands that delete or overwrite the contents of `key`. - See https://redis.io/commands/pexpireat/ for more details. + See https://valkey.io/commands/pexpireat/ for more details. Args: - key (str): The key to set a timeout on. + key (TEncodable): The key to set a timeout on. unix_milliseconds (int): The timeout in an absolute Unix timestamp in milliseconds. option (Optional[ExpireOptions]): The expire option. @@ -1115,13 +1742,48 @@ def pexpireat( ) return self.append_command(RequestType.PExpireAt, args) - def ttl(self: TTransaction, key: str) -> TTransaction: + def expiretime(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the absolute Unix timestamp (since January 1, 1970) at which + the given `key` will expire, in seconds. + To get the expiration with millisecond precision, use `pexpiretime`. + + See https://valkey.io/commands/expiretime/ for details. + + Args: + key (TEncodable): The `key` to determine the expiration value of. + + Commands response: + int: The expiration Unix timestamp in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. + + Since: Valkey version 7.0.0. + """ + return self.append_command(RequestType.ExpireTime, [key]) + + def pexpiretime(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the absolute Unix timestamp (since January 1, 1970) at which + the given `key` will expire, in milliseconds. + + See https://valkey.io/commands/pexpiretime/ for details. + + Args: + key (TEncodable): The `key` to determine the expiration value of. + + Commands response: + int: The expiration Unix timestamp in milliseconds, -2 if `key` does not exist, or -1 if `key` exists but has no associated expiration. + + Since: Valkey version 7.0.0. + """ + return self.append_command(RequestType.PExpireTime, [key]) + + def ttl(self: TTransaction, key: TEncodable) -> TTransaction: """ Returns the remaining time to live of `key` that has a timeout. - See https://redis.io/commands/ttl/ for more details. + See https://valkey.io/commands/ttl/ for more details. Args: - key (str): The key to return its timeout. + key (TEncodable): The key to return its timeout. Commands response: int: TTL in seconds, -2 if `key` does not exist or -1 if `key` exists but has no associated expire. @@ -1130,14 +1792,14 @@ def ttl(self: TTransaction, key: str) -> TTransaction: def pttl( self: TTransaction, - key: str, + key: TEncodable, ) -> TTransaction: """ Returns the remaining time to live of `key` that has a timeout, in milliseconds. - See https://redis.io/commands/pttl for more details. + See https://valkey.io/commands/pttl for more details. Args: - key (str): The key to return its timeout. + key (TEncodable): The key to return its timeout. Commands Response: int: TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. @@ -1146,612 +1808,3368 @@ def pttl( def persist( self: TTransaction, - key: str, + key: TEncodable, ) -> TTransaction: """ Remove the existing timeout on `key`, turning the key from volatile (a key with an expire set) to persistent (a key that will never expire as no timeout is associated). - See https://redis.io/commands/persist/ for more details. + See https://valkey.io/commands/persist/ for more details. Args: - key (str): TThe key to remove the existing timeout on. + key (TEncodable): The key to remove the existing timeout on. Commands response: bool: False if `key` does not exist or does not have an associated timeout, True if the timeout has been removed. """ return self.append_command(RequestType.Persist, [key]) - def echo(self: TTransaction, message: str) -> TTransaction: + def echo(self: TTransaction, message: TEncodable) -> TTransaction: """ Echoes the provided `message` back. - See https://redis.io/commands/echo for more details. + See https://valkey.io/commands/echo for more details. Args: - message (str): The message to be echoed back. + message (TEncodable): The message to be echoed back. Commands response: - str: The provided `message`. + bytes: The provided `message`. """ return self.append_command(RequestType.Echo, [message]) - def type(self: TTransaction, key: str) -> TTransaction: + def lastsave(self: TTransaction) -> TTransaction: + """ + Returns the Unix time of the last DB save timestamp or startup timestamp if no save was made since then. + + See https://valkey.io/commands/lastsave for more details. + + Command response: + int: The Unix time of the last successful DB save. + """ + return self.append_command(RequestType.LastSave, []) + + def type(self: TTransaction, key: TEncodable) -> TTransaction: """ Returns the string representation of the type of the value stored at `key`. - See https://redis.io/commands/type/ for more details. + See https://valkey.io/commands/type/ for more details. - Args: - key (str): The key to check its data type. + Args: + key (TEncodable): The key to check its data type. Commands response: - str: If the key exists, the type of the stored value is returned. + bytes: If the key exists, the type of the stored value is returned. Otherwise, a "none" string is returned. """ return self.append_command(RequestType.Type, [key]) - def geoadd( - self: TTransaction, - key: str, - members_geospatialdata: Mapping[str, GeospatialData], - existing_options: Optional[ConditionalChange] = None, - changed: bool = False, + def function_load( + self: TTransaction, library_code: TEncodable, replace: bool = False ) -> TTransaction: """ - Adds geospatial members with their positions to the specified sorted set stored at `key`. - If a member is already a part of the sorted set, its position is updated. + Loads a library to Valkey. - See https://valkey.io/commands/geoadd for more details. + See https://valkey.io/commands/function-load/ for more details. Args: - key (str): The key of the sorted set. - members_geospatialdata (Mapping[str, GeospatialData]): A mapping of member names to their corresponding positions. See `GeospatialData`. - The command will report an error when the user attempts to index coordinates outside the specified ranges. - existing_options (Optional[ConditionalChange]): Options for handling existing members. - - NX: Only add new elements. - - XX: Only update existing elements. - changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + library_code (TEncodable): The source code that implements the library. + replace (bool): Whether the given library should overwrite a library with the same name if + it already exists. Commands response: - int: The number of elements added to the sorted set. - If `changed` is set, returns the number of elements updated in the sorted set. - """ - args = [key] - if existing_options: - args.append(existing_options.value) - - if changed: - args.append("CH") - - members_geospatialdata_list = [ - coord - for member, position in members_geospatialdata.items() - for coord in [str(position.longitude), str(position.latitude), member] - ] - args += members_geospatialdata_list + bytes: The library name that was loaded. - return self.append_command(RequestType.GeoAdd, args) + Since: Valkey 7.0.0. + """ + return self.append_command( + RequestType.FunctionLoad, + ["REPLACE", library_code] if replace else [library_code], + ) - def geohash(self: TTransaction, key: str, members: List[str]) -> TTransaction: + def function_list( + self: TTransaction, + library_name_pattern: Optional[TEncodable] = None, + with_code: bool = False, + ) -> TTransaction: """ - Returns the GeoHash strings representing the positions of all the specified members in the sorted set stored at - `key`. + Returns information about the functions and libraries. - See https://valkey.io/commands/geohash for more details. + See https://valkey.io/commands/function-list/ for more details. Args: - key (str): The key of the sorted set. - members (List[str]): The list of members whose GeoHash strings are to be retrieved. + library_name_pattern (Optional[TEncodable]): A wildcard pattern for matching library names. + with_code (bool): Specifies whether to request the library code from the server or not. Commands response: - List[Optional[str]]: A list of GeoHash strings representing the positions of the specified members stored at `key`. - If a member does not exist in the sorted set, a None value is returned for that member. + TFunctionListResponse: Info about all or + selected libraries and their functions. + + Since: Valkey 7.0.0. """ - return self.append_command(RequestType.GeoHash, [key] + members) + args = [] + if library_name_pattern is not None: + args.extend(["LIBRARYNAME", library_name_pattern]) + if with_code: + args.append("WITHCODE") + return self.append_command( + RequestType.FunctionList, + args, + ) - def zadd( - self: TTransaction, - key: str, - members_scores: Mapping[str, float], - existing_options: Optional[ConditionalChange] = None, - update_condition: Optional[UpdateOptions] = None, - changed: bool = False, + def function_flush( + self: TTransaction, mode: Optional[FlushMode] = None ) -> TTransaction: """ - Adds members with their scores to the sorted set stored at `key`. - If a member is already a part of the sorted set, its score is updated. + Deletes all function libraries. - See https://redis.io/commands/zadd/ for more details. + See https://valkey.io/commands/function-flush/ for more details. Args: - key (str): The key of the sorted set. - members_scores (Mapping[str, float]): A mapping of members to their corresponding scores. - existing_options (Optional[ConditionalChange]): Options for handling existing members. - - NX: Only add new elements. - - XX: Only update existing elements. - update_condition (Optional[UpdateOptions]): Options for updating scores. - - GT: Only update scores greater than the current values. - - LT: Only update scores less than the current values. - changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. Commands response: - int: The number of elements added to the sorted set. - If `changed` is set, returns the number of elements updated in the sorted set. + TOK: A simple `OK`. + + Since: Valkey 7.0.0. """ - args = [key] - if existing_options: - args.append(existing_options.value) + return self.append_command( + RequestType.FunctionFlush, + [mode.value] if mode else [], + ) - if update_condition: - args.append(update_condition.value) + def function_delete(self: TTransaction, library_name: TEncodable) -> TTransaction: + """ + Deletes a library and all its functions. - if changed: - args.append("CH") + See https://valkey.io/commands/function-delete/ for more details. - if existing_options and update_condition: - if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: - raise ValueError( - "The GT, LT and NX options are mutually exclusive. " - f"Cannot choose both {update_condition.value} and NX." - ) + Args: + library_code (TEncodable): The library name to delete - members_scores_list = [ - str(item) for pair in members_scores.items() for item in pair[::-1] - ] - args += members_scores_list + Commands response: + TOK: A simple `OK`. - return self.append_command(RequestType.Zadd, args) + Since: Valkey 7.0.0. + """ + return self.append_command( + RequestType.FunctionDelete, + [library_name], + ) - def zadd_incr( + def fcall( self: TTransaction, - key: str, - member: str, - increment: float, - existing_options: Optional[ConditionalChange] = None, - update_condition: Optional[UpdateOptions] = None, + function: TEncodable, + keys: Optional[List[TEncodable]] = None, + arguments: Optional[List[TEncodable]] = None, ) -> TTransaction: """ - Increments the score of member in the sorted set stored at `key` by `increment`. - If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). - If `key` does not exist, a new sorted set with the specified member as its sole member is created. + Invokes a previously loaded function. + See https://valkey.io/commands/fcall/ for more details. + Args: + function (TEncodable): The function name. + keys (Optional[List[TEncodable]]): A list of keys accessed by the function. To ensure the correct + execution of functions, both in standalone and clustered deployments, all names of keys + that a function accesses must be explicitly provided as `keys`. + arguments (Optional[List[TEncodable]]): A list of `function` arguments. `Arguments` + should not represent names of keys. + Command Response: + TResult: + The invoked function's return value. + Since: Valkey version 7.0.0. + """ + args = [] + if keys is not None: + args.extend([function, str(len(keys))] + keys) + else: + args.extend([function, str(0)]) + if arguments is not None: + args.extend(arguments) + return self.append_command(RequestType.FCall, args) + + def fcall_ro( + self: TTransaction, + function: TEncodable, + keys: Optional[List[TEncodable]] = None, + arguments: Optional[List[TEncodable]] = None, + ) -> TTransaction: + """ + Invokes a previously loaded read-only function. - See https://redis.io/commands/zadd/ for more details. + See https://valkey.io/commands/fcall_ro for more details. Args: - key (str): The key of the sorted set. - member (str): A member in the sorted set to increment. - increment (float): The score to increment the member. - existing_options (Optional[ConditionalChange]): Options for handling the member's existence. - - NX: Only increment a member that doesn't exist. - - XX: Only increment an existing member. - update_condition (Optional[UpdateOptions]): Options for updating the score. - - GT: Only increment the score of the member if the new score will be greater than the current score. - - LT: Only increment (decrement) the score of the member if the new score will be less than the current score. + function (TEncodable): The function name. + keys (List[TEncodable]): An `array` of keys accessed by the function. To ensure the correct + execution of functions, all names of keys that a function accesses must be + explicitly provided as `keys`. + arguments (List[TEncodable]): An `array` of `function` arguments. `arguments` should not + represent names of keys. - Commands response: - Optional[float]: The score of the member. - If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. + Command Response: + TResult: The return value depends on the function that was executed. + + Since: Valkey version 7.0.0. """ - args = [key] - if existing_options: - args.append(existing_options.value) + args = [] + if keys is not None: + args.extend([function, str(len(keys))] + keys) + else: + args.extend([function, str(0)]) + if arguments is not None: + args.extend(arguments) + return self.append_command(RequestType.FCallReadOnly, args) - if update_condition: - args.append(update_condition.value) + def function_stats(self: TTransaction) -> TTransaction: + """ + Returns information about the function that's currently running and information about the + available execution engines. - args.append("INCR") + See https://valkey.io/commands/function-stats/ for more details - if existing_options and update_condition: - if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: - raise ValueError( - "The GT, LT and NX options are mutually exclusive. " - f"Cannot choose both {update_condition.value} and NX." - ) + Command Response: + TFunctionStatsResponse: A `Mapping` with two keys: + - `running_script` with information about the running script. + - `engines` with information about available engines and their stats. + See example for more details. - args += [str(increment), member] - return self.append_command(RequestType.Zadd, args) + Since: Valkey version 7.0.0. + """ + return self.append_command(RequestType.FunctionStats, []) - def zcard(self: TTransaction, key: str) -> TTransaction: + def function_dump(self: TTransaction) -> TTransaction: """ - Returns the cardinality (number of elements) of the sorted set stored at `key`. + Returns the serialized payload of all loaded libraries. - See https://redis.io/commands/zcard/ for more details. + See https://valkey.io/commands/function-dump/ for more details. - Args: - key (str): The key of the sorted set. + Command response: + bytes: The serialized payload of all loaded libraries. - Commands response: - int: The number of elements in the sorted set. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + Since: Valkey version 7.0.0. """ - return self.append_command(RequestType.Zcard, [key]) + return self.append_command(RequestType.FunctionDump, []) - def zcount( + def function_restore( self: TTransaction, - key: str, - min_score: Union[InfBound, ScoreBoundary], - max_score: Union[InfBound, ScoreBoundary], + payload: TEncodable, + policy: Optional[FunctionRestorePolicy] = None, ) -> TTransaction: """ - Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. + Restores libraries from the serialized payload returned by the `function_dump` command. - See https://redis.io/commands/zcount/ for more details. + See https://valkey.io/commands/function-restore/ for more details. Args: - key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. + payload (TEncodable): The serialized data from the `function_dump` command. + policy (Optional[FunctionRestorePolicy]): A policy for handling existing libraries. - Commands response: - int: The number of members in the specified score range. - If key does not exist, 0 is returned. - If `max_score` < `min_score`, 0 is returned. + Command response: + TOK: A simple "OK" response. + + Since: Valkey version 7.0.0. """ - score_min = ( - min_score.value["score_arg"] - if type(min_score) == InfBound - else min_score.value - ) - score_max = ( - max_score.value["score_arg"] - if type(max_score) == InfBound - else max_score.value - ) - return self.append_command(RequestType.Zcount, [key, score_min, score_max]) + args: List[TEncodable] = [payload] + if policy is not None: + args.append(policy.value) + return self.append_command(RequestType.FunctionRestore, args) - def zpopmax( - self: TTransaction, key: str, count: Optional[int] = None + def dump( + self: TTransaction, + key: TEncodable, ) -> TTransaction: """ - Removes and returns the members with the highest scores from the sorted set stored at `key`. - If `count` is provided, up to `count` members with the highest scores are removed and returned. - Otherwise, only one member with the highest score is removed and returned. + Serialize the value stored at `key` in a Valkey-specific format and return it to the user. - See https://redis.io/commands/zpopmax for more details. + See https://valkey.io/commands/dump/ for more details. Args: - key (str): The key of the sorted set. - count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. - If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. + key (TEncodable): The `key` to serialize. - Commands response: - Mapping[str, float]: A map of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. - If `key` doesn't exist, it will be treated as an empy sorted set and the command returns an empty map. + Command response: + Optional[bytes]: The serialized value of the data stored at `key`. If `key` does not + exist, `None` will be returned. """ - return self.append_command( - RequestType.ZPopMax, [key, str(count)] if count else [key] - ) + return self.append_command(RequestType.Dump, [key]) - def zpopmin( - self: TTransaction, key: str, count: Optional[int] = None + def restore( + self: TTransaction, + key: TEncodable, + ttl: int, + value: TEncodable, + replace: bool = False, + absttl: bool = False, + idletime: Optional[int] = None, + frequency: Optional[int] = None, ) -> TTransaction: """ - Removes and returns the members with the lowest scores from the sorted set stored at `key`. - If `count` is provided, up to `count` members with the lowest scores are removed and returned. - Otherwise, only one member with the lowest score is removed and returned. + Create a `key` associated with a `value` that is obtained by deserializing the provided + serialized `value` obtained via `dump`. - See https://redis.io/commands/zpopmin for more details. + See https://valkey.io/commands/restore for more details. Args: - key (str): The key of the sorted set. - count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. - If `count` is higher than the sorted set's cardinality, returns all members and their scores. + key (TEncodable): The `key` to create. + ttl (int): The expiry time (in milliseconds). If `0`, the `key` will persist. + value (TEncodable) The serialized value to deserialize and assign to `key`. + replace (bool): Set to `True` to replace the key if it exists. + absttl (bool): Set to `True` to specify that `ttl` represents an absolute Unix + timestamp (in milliseconds). + idletime (Optional[int]): Set the `IDLETIME` option with object idletime to the given key. + frequency (Optional[int]): Set the `FREQ` option with object frequency to the given key. - Commands response: - Mapping[str, float]: A map of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. - If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. + Command response: + TOK: A simple "OK" response. """ - return self.append_command( - RequestType.ZPopMin, [key, str(count)] if count else [key] - ) - - def zrange( + args = [key, str(ttl), value] + if replace is True: + args.append("REPLACE") + if absttl is True: + args.append("ABSTTL") + if idletime is not None: + args.extend(["IDLETIME", str(idletime)]) + if frequency is not None: + args.extend(["FREQ", str(frequency)]) + return self.append_command(RequestType.Restore, args) + + def xadd( self: TTransaction, - key: str, - range_query: Union[RangeByIndex, RangeByLex, RangeByScore], - reverse: bool = False, + key: TEncodable, + values: List[Tuple[TEncodable, TEncodable]], + options: StreamAddOptions = StreamAddOptions(), ) -> TTransaction: """ - Returns the specified range of elements in the sorted set stored at `key`. - - ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + Adds an entry to the specified stream stored at `key`. If the `key` doesn't exist, the stream is created. - See https://redis.io/commands/zrange/ for more details. + See https://valkey.io/commands/xadd for more details. Args: - key (str): The key of the sorted set. - range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. - - For range queries by index (rank), use RangeByIndex. - - For range queries by lexicographical order, use RangeByLex. - - For range queries by score, use RangeByScore. - reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + key (TEncodable): The key of the stream. + values: List[Tuple[TEncodable, TEncodable]]: Field-value pairs to be added to the entry. + options (Optional[StreamAddOptions]): Additional options for adding entries to the stream. Default to None. See `StreamAddOptions`. Commands response: - List[str]: A list of elements within the specified range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + bytes: The id of the added entry, or None if `options.make_stream` is set to False and no stream with the matching `key` exists. """ - args = _create_zrange_args(key, range_query, reverse, with_scores=False) + args = [key] + if options: + args.extend(options.to_args()) + args.extend([field for pair in values for field in pair]) - return self.append_command(RequestType.Zrange, args) + return self.append_command(RequestType.XAdd, args) - def zrange_withscores( + def xdel( + self: TTransaction, key: TEncodable, ids: List[TEncodable] + ) -> TTransaction: + """ + Removes the specified entries by id from a stream, and returns the number of entries deleted. + + See https://valkey.io/commands/xdel for more details. + + Args: + key (TEncodable): The key of the stream. + ids (List[TEncodable]): An array of entry ids. + + Command response: + int: The number of entries removed from the stream. This number may be less than the number of entries in + `ids`, if the specified `ids` don't exist in the stream. + """ + return self.append_command(RequestType.XDel, [key] + ids) + + def xtrim( self: TTransaction, - key: str, - range_query: Union[RangeByIndex, RangeByScore], - reverse: bool = False, + key: TEncodable, + options: StreamTrimOptions, ) -> TTransaction: """ - Returns the specified range of elements with their scores in the sorted set stored at `key`. - Similar to ZRANGE but with a WITHSCORE flag. + Trims the stream stored at `key` by evicting older entries. - See https://redis.io/commands/zrange/ for more details. + See https://valkey.io/commands/xtrim for more details. Args: - key (str): The key of the sorted set. - range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. - - For range queries by index (rank), use RangeByIndex. - - For range queries by score, use RangeByScore. - reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + key (TEncodable): The key of the stream. + options (StreamTrimOptions): Options detailing how to trim the stream. See `StreamTrimOptions`. Commands response: - Mapping[str , float]: A map of elements and their scores within the specified range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + int: TThe number of entries deleted from the stream. If `key` doesn't exist, 0 is returned. """ - args = _create_zrange_args(key, range_query, reverse, with_scores=True) + args = [key] + if options: + args.extend(options.to_args()) - return self.append_command(RequestType.Zrange, args) + return self.append_command(RequestType.XTrim, args) - def zrank( + def xlen(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the number of entries in the stream stored at `key`. + + See https://valkey.io/commands/xlen for more details. + + Args: + key (TEncodable): The key of the stream. + + Command response: + int: The number of entries in the stream. If `key` does not exist, returns 0. + """ + return self.append_command(RequestType.XLen, [key]) + + def xrange( self: TTransaction, - key: str, - member: str, + key: TEncodable, + start: StreamRangeBound, + end: StreamRangeBound, + count: Optional[int] = None, ) -> TTransaction: """ - Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. - - See https://redis.io/commands/zrank for more details. + Returns stream entries matching a given range of IDs. - To get the rank of `member` with its score, see `zrank_withscore`. + See https://valkey.io/commands/xrange for more details. Args: - key (str): The key of the sorted set. - member (str): The member whose rank is to be retrieved. + key (TEncodable): The key of the stream. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. - Commands response: - Optional[int]: The rank of `member` in the sorted set. - If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + Command response: + Optional[Mapping[bytes, List[List[bytes]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments + are not applicable. """ - return self.append_command(RequestType.Zrank, [key, member]) + args = [key, start.to_arg(), end.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) - def zrank_withscore( + return self.append_command(RequestType.XRange, args) + + def xrevrange( self: TTransaction, - key: str, - member: str, + key: TEncodable, + end: StreamRangeBound, + start: StreamRangeBound, + count: Optional[int] = None, ) -> TTransaction: """ - Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. + Returns stream entries matching a given range of IDs in reverse order. Equivalent to `XRANGE` but returns the + entries in reverse order. - See https://redis.io/commands/zrank for more details. + See https://valkey.io/commands/xrevrange for more details. Args: - key (str): The key of the sorted set. - member (str): The member whose rank is to be retrieved. + key (TEncodable): The key of the stream. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. - Commands response: - Optional[List[Union[int, float]]]: A list containing the rank and score of `member` in the sorted set. - If `key` doesn't exist, or if `member` is not present in the set, None will be returned. - - Since: Redis version 7.2.0. + Command response: + Optional[Mapping[bytes, List[List[bytes]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments + are not applicable. """ - return self.append_command(RequestType.Zrank, [key, member, "WITHSCORE"]) + args = [key, end.to_arg(), start.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) - def zrem( + return self.append_command(RequestType.XRevRange, args) + + def xread( self: TTransaction, - key: str, - members: List[str], + keys_and_ids: Mapping[TEncodable, TEncodable], + options: Optional[StreamReadOptions] = None, ) -> TTransaction: """ - Removes the specified members from the sorted set stored at `key`. - Specified members that are not a member of this set are ignored. + Reads entries from the given streams. - See https://redis.io/commands/zrem/ for more details. + See https://valkey.io/commands/xread for more details. Args: - key (str): The key of the sorted set. - members (List[str]): A list of members to remove from the sorted set. + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of keys and entry IDs to read from. The mapping is composed of a + stream's key and the ID of the entry after which the stream will be read. + options (Optional[StreamReadOptions]): Options detailing how to read the stream. - Commands response: - int: The number of members that were removed from the sorted set, not including non-existing members. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + Command response: + Optional[Mapping[bytes, Mapping[bytes, List[List[bytes]]]]]: A mapping of stream keys, to a mapping of stream IDs, + to a list of pairings with format `[[field, entry], [field, entry], ...]`. + None will be returned under the following conditions: + - All key-ID pairs in `keys_and_ids` have either a non-existing key or a non-existing ID, or there are no entries after the given ID. + - The `BLOCK` option is specified and the timeout is hit. """ - return self.append_command(RequestType.Zrem, [key] + members) + args: List[TEncodable] = [] if options is None else options.to_args() + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) - def zremrangebyscore( + return self.append_command(RequestType.XRead, args) + + def xgroup_create( self: TTransaction, - key: str, - min_score: Union[InfBound, ScoreBoundary], - max_score: Union[InfBound, ScoreBoundary], + key: TEncodable, + group_name: TEncodable, + group_id: TEncodable, + options: Optional[StreamGroupOptions] = None, ) -> TTransaction: """ - Removes all elements in the sorted set stored at `key` with a score between `min_score` and `max_score`. + Creates a new consumer group uniquely identified by `group_name` for the stream stored at `key`. - See https://redis.io/commands/zremrangebyscore/ for more details. + See https://valkey.io/commands/xgroup-create for more details. Args: - key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreBoundary]): The minimum score to remove from. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreBoundary]): The maximum score to remove up to. - Can be an instance of InfBound representing positive/negative infinity, - or ScoreBoundary representing a specific score and inclusivity. + key (TEncodable): The key of the stream. + group_name (TEncodable): The newly created consumer group name. + group_id (TEncodable): The stream entry ID that specifies the last delivered entry in the stream from the new + group’s perspective. The special ID "$" can be used to specify the last entry in the stream. + options (Optional[StreamGroupOptions]): Options for creating the stream group. - Commands response: - int: The number of members that were removed from the sorted set. - If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. - If `min_score` is greater than `max_score`, 0 is returned. + Command response: + TOK: A simple "OK" response. """ - score_min = ( - min_score.value["score_arg"] - if type(min_score) == InfBound - else min_score.value - ) - score_max = ( - max_score.value["score_arg"] - if type(max_score) == InfBound - else max_score.value - ) - return self.append_command( - RequestType.ZRemRangeByScore, [key, score_min, score_max] - ) + args = [key, group_name, group_id] + if options is not None: + args.extend(options.to_args()) - def zremrangebylex( - self: TTransaction, - key: str, - min_lex: Union[InfBound, LexBoundary], - max_lex: Union[InfBound, LexBoundary], + return self.append_command(RequestType.XGroupCreate, args) + + def xgroup_destroy( + self: TTransaction, key: TEncodable, group_name: TEncodable ) -> TTransaction: """ - Removes all elements in the sorted set stored at `key` with a lexicographical order between `min_lex` and - `max_lex`. + Destroys the consumer group `group_name` for the stream stored at `key`. - See https://redis.io/commands/zremrangebylex/ for more details. + See https://valkey.io/commands/xgroup-destroy for more details. Args: - key (str): The key of the sorted set. - min_lex (Union[InfBound, LexBoundary]): The minimum bound of the lexicographical range. - Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` - representing a specific lex and inclusivity. - max_lex (Union[InfBound, LexBoundary]): The maximum bound of the lexicographical range. - Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` - representing a specific lex and inclusivity. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name to delete. Command response: - int: The number of members that were removed from the sorted set. - If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. - If `min_lex` is greater than `max_lex`, `0` is returned. + bool: True if the consumer group was destroyed. Otherwise, returns False. """ - min_lex_arg = ( - min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value - ) - max_lex_arg = ( - max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value - ) + return self.append_command(RequestType.XGroupDestroy, [key, group_name]) + + def xgroup_create_consumer( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + ) -> TTransaction: + """ + Creates a consumer named `consumer_name` in the consumer group `group_name` for the stream stored at `key`. + + See https://valkey.io/commands/xgroup-createconsumer for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The newly created consumer. + Command response: + bool: True if the consumer is created. Otherwise, returns False. + """ return self.append_command( - RequestType.ZRemRangeByLex, [key, min_lex_arg, max_lex_arg] + RequestType.XGroupCreateConsumer, [key, group_name, consumer_name] ) - def zlexcount( + def xgroup_del_consumer( self: TTransaction, - key: str, - min_lex: Union[InfBound, LexBoundary], - max_lex: Union[InfBound, LexBoundary], + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, ) -> TTransaction: """ - Returns the number of members in the sorted set stored at `key` with lexographical values between `min_lex` and `max_lex`. + Deletes a consumer named `consumer_name` in the consumer group `group_name` for the stream stored at `key`. - See https://redis.io/commands/zlexcount/ for more details. + See https://valkey.io/commands/xgroup-delconsumer for more details. Args: - key (str): The key of the sorted set. - min_lex (Union[InfBound, LexBoundary]): The minimum lexicographical value to count from. - Can be an instance of InfBound representing positive/negative infinity, - or LexBoundary representing a specific lexicographical value and inclusivity. - max_lex (Union[InfBound, LexBoundary]): The maximum lexicographical value to count up to. - Can be an instance of InfBound representing positive/negative infinity, - or LexBoundary representing a specific lexicographical value and inclusivity. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer to delete. Command response: - int: The number of members in the specified lexicographical range. - If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. - If `max_lex < min_lex`, `0` is returned. + int: The number of pending messages the `consumer` had before it was deleted. """ - min_lex_arg = ( - min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value - ) - max_lex_arg = ( - max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value - ) - return self.append_command( - RequestType.ZLexCount, [key, min_lex_arg, max_lex_arg] + RequestType.XGroupDelConsumer, [key, group_name, consumer_name] ) - def zscore(self: TTransaction, key: str, member: str) -> TTransaction: + def xgroup_set_id( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + stream_id: TEncodable, + entries_read_id: Optional[str] = None, + ) -> TTransaction: """ - Returns the score of `member` in the sorted set stored at `key`. + Set the last delivered ID for a consumer group. - See https://redis.io/commands/zscore/ for more details. + See https://valkey.io/commands/xgroup-setid for more details. Args: - key (str): The key of the sorted set. - member (str): The member whose score is to be retrieved. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + stream_id (TEncodable): The stream entry ID that should be set as the last delivered ID for the consumer group. + entries_read_id (Optional[str]): An arbitrary ID (that isn't the first ID, last ID, or the zero ID ("0-0")) + used to find out how many entries are between the arbitrary ID (excluding it) and the stream's last + entry. This argument can only be specified if you are using Valkey version 7.0.0 or above. - Commands response: - Optional[float]: The score of the member. - If `member` does not exist in the sorted set, None is returned. - If `key` does not exist, None is returned. + Command response: + TOK: A simple "OK" response. """ - return self.append_command(RequestType.ZScore, [key, member]) + args = [key, group_name, stream_id] + if entries_read_id is not None: + args.extend(["ENTRIESREAD", entries_read_id]) - def dbsize(self: TTransaction) -> TTransaction: - """ - Returns the number of keys in the currently selected database. - See https://redis.io/commands/dbsize for more details. + return self.append_command(RequestType.XGroupSetId, args) - Commands response: - int: The number of keys in the database. + def xreadgroup( + self: TTransaction, + keys_and_ids: Mapping[TEncodable, TEncodable], + group_name: TEncodable, + consumer_name: TEncodable, + options: Optional[StreamReadGroupOptions] = None, + ) -> TTransaction: """ - return self.append_command(RequestType.DBSize, []) + Reads entries from the given streams owned by a consumer group. + See https://valkey.io/commands/xreadgroup for more details. -class Transaction(BaseTransaction): - """ - Extends BaseTransaction class for standalone Redis commands that are not supported in Redis cluster mode. + Args: + keys_and_ids (Mapping[TEncodable, TEncodable]): A mapping of stream keys to stream entry IDs to read from. The special ">" + ID returns messages that were never delivered to any other consumer. Any other valid ID will return + entries pending for the consumer with IDs greater than the one provided. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer name. The consumer will be auto-created if it does not already exist. + options (Optional[StreamReadGroupOptions]): Options detailing how to read the stream. - Command Response: - The response for each command depends on the executed Redis command. Specific response types - are documented alongside each method. + Command response: + Optional[Mapping[bytes, Mapping[bytes, Optional[List[List[bytes]]]]]]: A mapping of stream keys, to a mapping of + stream IDs, to a list of pairings with format `[[field, entry], [field, entry], ...]`. + Returns None if the BLOCK option is given and a timeout occurs, or if there is no stream that can be served. + """ + args = ["GROUP", group_name, consumer_name] + if options is not None: + args.extend(options.to_args()) - Example: - transaction = Transaction() - >>> transaction.set("key", "value") - >>> transaction.select(1) # Standalone command - >>> transaction.get("key") - >>> await client.exec(transaction) - [OK , OK , None] + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) - """ + return self.append_command(RequestType.XReadGroup, args) - # TODO: add MOVE, SLAVEOF and all SENTINEL commands - def select(self, index: int) -> "Transaction": + def xack( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + ids: List[TEncodable], + ) -> TTransaction: """ - Change the currently selected Redis database. - See https://redis.io/commands/select/ for details. + Removes one or multiple messages from the Pending Entries List (PEL) of a stream consumer group. + This command should be called on pending messages so that such messages do not get processed again by the + consumer group. + + See https://valkey.io/commands/xack for more details. Args: - index (int): The index of the database to select. + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + ids (List[TEncodable]): The stream entry IDs to acknowledge and consume for the given consumer group. Command response: - A simple OK response. + int: The number of messages that were successfully acknowledged. """ - return self.append_command(RequestType.Select, [str(index)]) + return self.append_command(RequestType.XAck, [key, group_name] + ids) + def xpending( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + ) -> TTransaction: + """ + Returns stream message summary information for pending messages for the given consumer group. -class ClusterTransaction(BaseTransaction): - """ - Extends BaseTransaction class for cluster mode commands that are not supported in standalone. + See https://valkey.io/commands/xpending for more details. - Command Response: - The response for each command depends on the executed Redis command. Specific response types - are documented alongside each method. + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + + Command response: + List[Union[int, bytes, List[List[bytes]], None]]: A list that includes the summary of pending messages, with the + format `[num_group_messages, start_id, end_id, [[consumer_name, num_consumer_messages]]]`, where: + - `num_group_messages`: The total number of pending messages for this consumer group. + - `start_id`: The smallest ID among the pending messages. + - `end_id`: The greatest ID among the pending messages. + - `[[consumer_name, num_consumer_messages]]`: A 2D list of every consumer in the consumer group with at + least one pending message, and the number of pending messages it has. + + If there are no pending messages for the given consumer group, `[0, None, None, None]` will be returned. + """ + return self.append_command(RequestType.XPending, [key, group_name]) + + def xpending_range( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + start: StreamRangeBound, + end: StreamRangeBound, + count: int, + options: Optional[StreamPendingOptions] = None, + ) -> TTransaction: + """ + Returns an extended form of stream message information for pending messages matching a given range of IDs. + + See https://valkey.io/commands/xpending for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + count (int): Limits the number of messages returned. + options (Optional[StreamPendingOptions]): The stream pending options. + + Command response: + List[List[Union[bytes, int]]]: A list of lists, where each inner list is a length 4 list containing extended + message information with the format `[[id, consumer_name, time_elapsed, num_delivered]]`, where: + - `id`: The ID of the message. + - `consumer_name`: The name of the consumer that fetched the message and has still to acknowledge it. We + call it the current owner of the message. + - `time_elapsed`: The number of milliseconds that elapsed since the last time this message was delivered + to this consumer. + - `num_delivered`: The number of times this message was delivered. + """ + args = _create_xpending_range_args(key, group_name, start, end, count, options) + return self.append_command(RequestType.XPending, args) + + def xautoclaim( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + min_idle_time_ms: int, + start: TEncodable, + count: Optional[int] = None, + ) -> TTransaction: + """ + Transfers ownership of pending stream entries that match the specified criteria. + + See https://valkey.io/commands/xautoclaim for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer name. + min_idle_time_ms (int): Filters the claimed entries to those that have been idle for more than the specified + value. + start (TEncodable): Filters the claimed entries to those that have an ID equal or greater than the specified value. + count (Optional[int]): Limits the number of claimed entries to the specified value. + + Command response: + List[Union[str, Mapping[bytes, List[List[bytes]]], List[bytes]]]: A list containing the following elements: + - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is equivalent + to the next ID in the stream after the entries that were scanned, or "0-0" if the entire stream was + scanned. + - A mapping of the claimed entries, with the keys being the claimed entry IDs and the values being a + 2D list of the field-value pairs in the format `[[field1, value1], [field2, value2], ...]`. + - If you are using Valkey 7.0.0 or above, the response list will also include a list containing the + message IDs that were in the Pending Entries List but no longer exist in the stream. These IDs are + deleted from the Pending Entries List. + + Since: Valkey version 6.2.0. + """ + args = [key, group_name, consumer_name, str(min_idle_time_ms), start] + if count is not None: + args.extend(["COUNT", str(count)]) + + return self.append_command(RequestType.XAutoClaim, args) + + def xautoclaim_just_id( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + consumer_name: TEncodable, + min_idle_time_ms: int, + start: TEncodable, + count: Optional[int] = None, + ) -> TTransaction: + """ + Transfers ownership of pending stream entries that match the specified criteria. This command uses the JUSTID + argument to further specify that the return value should contain a list of claimed IDs without their + field-value info. + + See https://valkey.io/commands/xautoclaim for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + consumer_name (TEncodable): The consumer name. + min_idle_time_ms (int): Filters the claimed entries to those that have been idle for more than the specified + value. + start (TEncodable): Filters the claimed entries to those that have an ID equal or greater than the specified value. + count (Optional[int]): Limits the number of claimed entries to the specified value. + + Command response: + List[Union[bytes, List[bytes]]]: A list containing the following elements: + - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is equivalent + to the next ID in the stream after the entries that were scanned, or "0-0" if the entire stream was + scanned. + - A list of the IDs for the claimed entries. + - If you are using Valkey 7.0.0 or above, the response list will also include a list containing the + message IDs that were in the Pending Entries List but no longer exist in the stream. These IDs are + deleted from the Pending Entries List. + + Since: Valkey version 6.2.0. + """ + args = [key, group_name, consumer_name, str(min_idle_time_ms), start] + if count is not None: + args.extend(["COUNT", str(count)]) + + args.append("JUSTID") + + return self.append_command(RequestType.XAutoClaim, args) + + def xinfo_groups( + self: TTransaction, + key: TEncodable, + ) -> TTransaction: + """ + Returns the list of all consumer groups and their attributes for the stream stored at `key`. + + See https://valkey.io/commands/xinfo-groups for more details. + + Args: + key (TEncodable): The key of the stream. + + Command response: + List[Mapping[bytes, Union[bytes, int, None]]]: A list of mappings, where each mapping represents the + attributes of a consumer group for the stream at `key`. + """ + return self.append_command(RequestType.XInfoGroups, [key]) + + def xinfo_consumers( + self: TTransaction, + key: TEncodable, + group_name: TEncodable, + ) -> TTransaction: + """ + Returns the list of all consumers and their attributes for the given consumer group of the stream stored at + `key`. + + See https://valkey.io/commands/xinfo-consumers for more details. + + Args: + key (TEncodable): The key of the stream. + group_name (TEncodable): The consumer group name. + + Command response: + List[Mapping[bytes, Union[bytes, int]]]: A list of mappings, where each mapping contains the attributes of a + consumer for the given consumer group of the stream at `key`. + """ + return self.append_command(RequestType.XInfoConsumers, [key, group_name]) + + def xinfo_stream( + self: TTransaction, + key: TEncodable, + ) -> TTransaction: + """ + Returns information about the stream stored at `key`. To get more detailed information, use `xinfo_stream_full`. + + See https://valkey.io/commands/xinfo-stream for more details. + + Args: + key (TEncodable): The key of the stream. + + Command response: + TXInfoStreamResponse: A mapping of stream information for the given `key`. + """ + return self.append_command(RequestType.XInfoStream, [key]) + + def xinfo_stream_full( + self: TTransaction, + key: TEncodable, + count: Optional[int] = None, + ) -> TTransaction: + """ + Returns verbose information about the stream stored at `key`. + + See https://valkey.io/commands/xinfo-stream for more details. + + Args: + key (TEncodable): The key of the stream. + count (Optional[int]): The number of stream and PEL entries that are returned. A value of `0` means that all + entries will be returned. If not provided, defaults to `10`. + + Command response: + TXInfoStreamFullResponse: A mapping of detailed stream information for the given `key`. + """ + args = [key, "FULL"] + if count is not None: + args.extend(["COUNT", str(count)]) + + return self.append_command(RequestType.XInfoStream, args) + + def geoadd( + self: TTransaction, + key: TEncodable, + members_geospatialdata: Mapping[TEncodable, GeospatialData], + existing_options: Optional[ConditionalChange] = None, + changed: bool = False, + ) -> TTransaction: + """ + Adds geospatial members with their positions to the specified sorted set stored at `key`. + If a member is already a part of the sorted set, its position is updated. + + See https://valkey.io/commands/geoadd for more details. + + Args: + key (TEncodable): The key of the sorted set. + members_geospatialdata (Mapping[TEncodable, GeospatialData]): A mapping of member names to their corresponding positions. See `GeospatialData`. + The command will report an error when the user attempts to index coordinates outside the specified ranges. + existing_options (Optional[ConditionalChange]): Options for handling existing members. + - NX: Only add new elements. + - XX: Only update existing elements. + changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + + Commands response: + int: The number of elements added to the sorted set. + If `changed` is set, returns the number of elements updated in the sorted set. + """ + args = [key] + if existing_options: + args.append(existing_options.value) + + if changed: + args.append("CH") + + members_geospatialdata_list = [ + coord + for member, position in members_geospatialdata.items() + for coord in [str(position.longitude), str(position.latitude), member] + ] + args += members_geospatialdata_list + + return self.append_command(RequestType.GeoAdd, args) + + def geodist( + self: TTransaction, + key: TEncodable, + member1: TEncodable, + member2: TEncodable, + unit: Optional[GeoUnit] = None, + ) -> TTransaction: + """ + Returns the distance between two members in the geospatial index stored at `key`. + + See https://valkey.io/commands/geodist for more details. + + Args: + key (TEncodable): The key of the sorted set. + member1 (TEncodable): The name of the first member. + member2 (TEncodable): The name of the second member. + unit (Optional[GeoUnit]): The unit of distance measurement. See `GeoUnit`. + If not specified, the default unit is meters. + + Commands response: + Optional[float]: The distance between `member1` and `member2`. + If one or both members do not exist, or if the key does not exist, returns None. + """ + args = [key, member1, member2] + if unit: + args.append(unit.value) + + return self.append_command(RequestType.GeoDist, args) + + def geohash( + self: TTransaction, key: TEncodable, members: List[TEncodable] + ) -> TTransaction: + """ + Returns the GeoHash bytes strings representing the positions of all the specified members in the sorted set stored at + `key`. + + See https://valkey.io/commands/geohash for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): The list of members whose GeoHash bytes strings are to be retrieved. + + Commands response: + List[Optional[bytes]]: A list of GeoHash bytes strings representing the positions of the specified members stored at `key`. + If a member does not exist in the sorted set, a None value is returned for that member. + """ + return self.append_command(RequestType.GeoHash, [key] + members) + + def geopos( + self: TTransaction, + key: TEncodable, + members: List[TEncodable], + ) -> TTransaction: + """ + Returns the positions (longitude and latitude) of all the given members of a geospatial index in the sorted set stored at + `key`. + + See https://valkey.io/commands/geopos for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): The members for which to get the positions. + + Commands response: + List[Optional[List[float]]]: A list of positions (longitude and latitude) corresponding to the given members. + If a member does not exist, its position will be None. + """ + return self.append_command(RequestType.GeoPos, [key] + members) + + def geosearch( + self: TTransaction, + key: TEncodable, + search_from: Union[TEncodable, GeospatialData], + search_by: Union[GeoSearchByRadius, GeoSearchByBox], + order_by: Optional[OrderBy] = None, + count: Optional[GeoSearchCount] = None, + with_coord: bool = False, + with_dist: bool = False, + with_hash: bool = False, + ) -> TTransaction: + """ + Searches for members in a sorted set stored at `key` representing geospatial data within a circular or rectangular area. + + See https://valkey.io/commands/geosearch/ for more details. + + Args: + key (TEncodable): The key of the sorted set representing geospatial data. + search_from (Union[TEncodable, GeospatialData]): The location to search from. Can be specified either as a member + from the sorted set or as a geospatial data (see `GeospatialData`). + search_by (Union[GeoSearchByRadius, GeoSearchByBox]): The search criteria. + For circular area search, see `GeoSearchByRadius`. + For rectengal area search, see `GeoSearchByBox`. + order_by (Optional[OrderBy]): Specifies the order in which the results should be returned. + - `ASC`: Sorts items from the nearest to the farthest, relative to the center point. + - `DESC`: Sorts items from the farthest to the nearest, relative to the center point. + If not specified, the results would be unsorted. + count (Optional[GeoSearchCount]): Specifies the maximum number of results to return. See `GeoSearchCount`. + If not specified, return all results. + with_coord (bool): Whether to include coordinates of the returned items. Defaults to False. + with_dist (bool): Whether to include distance from the center in the returned items. + The distance is returned in the same unit as specified for the `search_by` arguments. Defaults to False. + with_hash (bool): Whether to include geohash of the returned items. Defaults to False. + + Command Response: + List[Union[bytes, List[Union[bytes, float, int, List[float]]]]]: By default, returns a list of members (locations) names. + If any of `with_coord`, `with_dist` or `with_hash` are True, returns an array of arrays, we're each sub array represents a single item in the following order: + (bytes): The member (location) name. + (float): The distance from the center as a floating point number, in the same unit specified in the radius, if `with_dist` is set to True. + (int): The Geohash integer, if `with_hash` is set to True. + List[float]: The coordinates as a two item [longitude,latitude] array, if `with_coord` is set to True. + + Since: Valkey version 6.2.0. + """ + args = _create_geosearch_args( + [key], + search_from, + search_by, + order_by, + count, + with_coord, + with_dist, + with_hash, + ) + + return self.append_command(RequestType.GeoSearch, args) + + def geosearchstore( + self: TTransaction, + destination: TEncodable, + source: TEncodable, + search_from: Union[TEncodable, GeospatialData], + search_by: Union[GeoSearchByRadius, GeoSearchByBox], + count: Optional[GeoSearchCount] = None, + store_dist: bool = False, + ) -> TTransaction: + """ + Searches for members in a sorted set stored at `key` representing geospatial data within a circular or rectangular area and stores the result in `destination`. + If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + + To get the result directly, see `geosearch`. + + See https://valkey.io/commands/geosearch/ for more details. + + Args: + destination (TEncodable): The key to store the search results. + source (TEncodable): The key of the sorted set representing geospatial data to search from. + search_from (Union[TEncodable, GeospatialData]): The location to search from. Can be specified either as a member + from the sorted set or as a geospatial data (see `GeospatialData`). + search_by (Union[GeoSearchByRadius, GeoSearchByBox]): The search criteria. + For circular area search, see `GeoSearchByRadius`. + For rectangular area search, see `GeoSearchByBox`. + count (Optional[GeoSearchCount]): Specifies the maximum number of results to store. See `GeoSearchCount`. + If not specified, stores all results. + store_dist (bool): Determines what is stored as the sorted set score. Defaults to False. + - If set to False, the geohash of the location will be stored as the sorted set score. + - If set to True, the distance from the center of the shape (circle or box) will be stored as the sorted set score. + The distance is represented as a floating-point number in the same unit specified for that shape. + + Commands response: + int: The number of elements in the resulting sorted set stored at `destination`.s + + Since: Valkey version 6.2.0. + """ + args = _create_geosearch_args( + [destination, source], + search_from, + search_by, + None, + count, + False, + False, + False, + store_dist, + ) + + return self.append_command(RequestType.GeoSearchStore, args) + + def zadd( + self: TTransaction, + key: TEncodable, + members_scores: Mapping[TEncodable, float], + existing_options: Optional[ConditionalChange] = None, + update_condition: Optional[UpdateOptions] = None, + changed: bool = False, + ) -> TTransaction: + """ + Adds members with their scores to the sorted set stored at `key`. + If a member is already a part of the sorted set, its score is updated. + + See https://valkey.io/commands/zadd/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + members_scores (Mapping[TEncodable, float]): A mapping of members to their corresponding scores. + existing_options (Optional[ConditionalChange]): Options for handling existing members. + - NX: Only add new elements. + - XX: Only update existing elements. + update_condition (Optional[UpdateOptions]): Options for updating scores. + - GT: Only update scores greater than the current values. + - LT: Only update scores less than the current values. + changed (bool): Modify the return value to return the number of changed elements, instead of the number of new elements added. + + Commands response: + int: The number of elements added to the sorted set. + If `changed` is set, returns the number of elements updated in the sorted set. + """ + args = [key] + if existing_options: + args.append(existing_options.value) + + if update_condition: + args.append(update_condition.value) + + if changed: + args.append("CH") + + if existing_options and update_condition: + if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: + raise ValueError( + "The GT, LT and NX options are mutually exclusive. " + f"Cannot choose both {update_condition.value} and NX." + ) + + members_scores_list = [ + str(item) for pair in members_scores.items() for item in pair[::-1] + ] + args += members_scores_list + + return self.append_command(RequestType.ZAdd, args) + + def zadd_incr( + self: TTransaction, + key: TEncodable, + member: TEncodable, + increment: float, + existing_options: Optional[ConditionalChange] = None, + update_condition: Optional[UpdateOptions] = None, + ) -> TTransaction: + """ + Increments the score of member in the sorted set stored at `key` by `increment`. + If `member` does not exist in the sorted set, it is added with `increment` as its score (as if its previous score was 0.0). + If `key` does not exist, a new sorted set with the specified member as its sole member is created. + + See https://valkey.io/commands/zadd/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): A member in the sorted set to increment. + increment (float): The score to increment the member. + existing_options (Optional[ConditionalChange]): Options for handling the member's existence. + - NX: Only increment a member that doesn't exist. + - XX: Only increment an existing member. + update_condition (Optional[UpdateOptions]): Options for updating the score. + - GT: Only increment the score of the member if the new score will be greater than the current score. + - LT: Only increment (decrement) the score of the member if the new score will be less than the current score. + + Commands response: + Optional[float]: The score of the member. + If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. + """ + args = [key] + if existing_options: + args.append(existing_options.value) + + if update_condition: + args.append(update_condition.value) + + args.append("INCR") + + if existing_options and update_condition: + if existing_options == ConditionalChange.ONLY_IF_DOES_NOT_EXIST: + raise ValueError( + "The GT, LT and NX options are mutually exclusive. " + f"Cannot choose both {update_condition.value} and NX." + ) + + args += [str(increment), member] + return self.append_command(RequestType.ZAdd, args) + + def zcard(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the cardinality (number of elements) of the sorted set stored at `key`. + + See https://valkey.io/commands/zcard/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + + Commands response: + int: The number of elements in the sorted set. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + """ + return self.append_command(RequestType.ZCard, [key]) + + def zcount( + self: TTransaction, + key: TEncodable, + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], + ) -> TTransaction: + """ + Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. + + See https://valkey.io/commands/zcount/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + + Commands response: + int: The number of members in the specified score range. + If key does not exist, 0 is returned. + If `max_score` < `min_score`, 0 is returned. + """ + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value + ) + return self.append_command(RequestType.ZCount, [key, score_min, score_max]) + + def zincrby( + self: TTransaction, + key: TEncodable, + increment: float, + member: TEncodable, + ) -> TTransaction: + """ + Increments the score of `member` in the sorted set stored at `key` by `increment`. + If `member` does not exist in the sorted set, it is added with `increment` as its score. + If `key` does not exist, a new sorted set is created with the specified member as its sole member. + + See https://valkey.io/commands/zincrby/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + increment (float): The score increment. + member (TEncodable): A member of the sorted set. + + Commands response: + float: The new score of `member`. + """ + return self.append_command(RequestType.ZIncrBy, [key, str(increment), member]) + + def zpopmax( + self: TTransaction, key: TEncodable, count: Optional[int] = None + ) -> TTransaction: + """ + Removes and returns the members with the highest scores from the sorted set stored at `key`. + If `count` is provided, up to `count` members with the highest scores are removed and returned. + Otherwise, only one member with the highest score is removed and returned. + + See https://valkey.io/commands/zpopmax for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. + If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. + + Commands response: + Mapping[bytes, float]: A map of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. + If `key` doesn't exist, it will be treated as an empy sorted set and the command returns an empty map. + """ + return self.append_command( + RequestType.ZPopMax, [key, str(count)] if count else [key] + ) + + def bzpopmax( + self: TTransaction, keys: List[TEncodable], timeout: float + ) -> TTransaction: + """ + Pops the member with the highest score from the first non-empty sorted set, with the given keys being checked in + the order that they are given. Blocks the connection when there are no members to remove from any of the given + sorted sets. + + `BZPOPMAX` is the blocking variant of `ZPOPMAX`. + + `BZPOPMAX` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/bzpopmax for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + timeout (float): The number of seconds to wait for a blocking operation to complete. + A value of 0 will block indefinitely. + + Command response: + Optional[List[Union[bytes, float]]]: An array containing the key where the member was popped out, the member itself, + and the member score. If no member could be popped and the `timeout` expired, returns None. + """ + return self.append_command(RequestType.BZPopMax, keys + [str(timeout)]) + + def zpopmin( + self: TTransaction, key: TEncodable, count: Optional[int] = None + ) -> TTransaction: + """ + Removes and returns the members with the lowest scores from the sorted set stored at `key`. + If `count` is provided, up to `count` members with the lowest scores are removed and returned. + Otherwise, only one member with the lowest score is removed and returned. + + See https://valkey.io/commands/zpopmin for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (Optional[int]): Specifies the quantity of members to pop. If not specified, pops one member. + If `count` is higher than the sorted set's cardinality, returns all members and their scores. + + Commands response: + Mapping[bytes, float]: A map of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. + If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. + """ + return self.append_command( + RequestType.ZPopMin, [key, str(count)] if count else [key] + ) + + def bzpopmin( + self: TTransaction, keys: List[TEncodable], timeout: float + ) -> TTransaction: + """ + Pops the member with the lowest score from the first non-empty sorted set, with the given keys being checked in + the order that they are given. Blocks the connection when there are no members to remove from any of the given + sorted sets. + + `BZPOPMIN` is the blocking variant of `ZPOPMIN`. + + `BZPOPMIN` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/bzpopmin for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + timeout (float): The number of seconds to wait for a blocking operation to complete. + A value of 0 will block indefinitely. + + Command response: + Optional[List[Union[bytes, float]]]: An array containing the key where the member was popped out, the member itself, + and the member score. If no member could be popped and the `timeout` expired, returns None. + """ + return self.append_command(RequestType.BZPopMin, keys + [str(timeout)]) + + def zrange( + self: TTransaction, + key: TEncodable, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Returns the specified range of elements in the sorted set stored at `key`. + + ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + + See https://valkey.io/commands/zrange/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Commands response: + List[bytes]: A list of elements within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=False) + + return self.append_command(RequestType.ZRange, args) + + def zrange_withscores( + self: TTransaction, + key: TEncodable, + range_query: Union[RangeByIndex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Returns the specified range of elements with their scores in the sorted set stored at `key`. + Similar to ZRANGE but with a WITHSCORE flag. + + See https://valkey.io/commands/zrange/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Commands response: + Mapping[bytes , float]: A map of elements and their scores within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=True) + + return self.append_command(RequestType.ZRange, args) + + def zrangestore( + self: TTransaction, + destination: TEncodable, + source: TEncodable, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Stores a specified range of elements from the sorted set at `source`, into a new sorted set at `destination`. If + `destination` doesn't exist, a new sorted set is created; if it exists, it's overwritten. + + ZRANGESTORE can perform different types of range queries: by index (rank), by the score, or by lexicographical + order. + + See https://valkey.io/commands/zrangestore for more details. + + Args: + destination (TEncodable): The key for the destination sorted set. + source (TEncodable): The key of the source sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Command response: + int: The number of elements in the resulting sorted set. + """ + args = _create_zrange_args(source, range_query, reverse, False, destination) + + return self.append_command(RequestType.ZRangeStore, args) + + def zrank( + self: TTransaction, + key: TEncodable, + member: TEncodable, + ) -> TTransaction: + """ + Returns the rank of `member` in the sorted set stored at `key`, with scores ordered from low to high. + + See https://valkey.io/commands/zrank for more details. + + To get the rank of `member` with its score, see `zrank_withscore`. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Commands response: + Optional[int]: The rank of `member` in the sorted set. + If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + """ + return self.append_command(RequestType.ZRank, [key, member]) + + def zrank_withscore( + self: TTransaction, + key: TEncodable, + member: TEncodable, + ) -> TTransaction: + """ + Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the lowest to highest. + + See https://valkey.io/commands/zrank for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Commands response: + Optional[List[Union[int, float]]]: A list containing the rank and score of `member` in the sorted set. + If `key` doesn't exist, or if `member` is not present in the set, None will be returned. + + Since: Valkey version 7.2.0. + """ + return self.append_command(RequestType.ZRank, [key, member, "WITHSCORE"]) + + def zrevrank( + self: TTransaction, key: TEncodable, member: TEncodable + ) -> TTransaction: + """ + Returns the rank of `member` in the sorted set stored at `key`, where scores are ordered from the highest to + lowest, starting from `0`. + + To get the rank of `member` with its score, see `zrevrank_withscore`. + + See https://valkey.io/commands/zrevrank for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Command response: + Optional[int]: The rank of `member` in the sorted set, where ranks are ordered from high to low based on scores. + If `key` doesn't exist, or if `member` is not present in the set, `None` will be returned. + """ + return self.append_command(RequestType.ZRevRank, [key, member]) + + def zrevrank_withscore( + self: TTransaction, key: TEncodable, member: TEncodable + ) -> TTransaction: + """ + Returns the rank of `member` in the sorted set stored at `key` with its score, where scores are ordered from the + highest to lowest, starting from `0`. + + See https://valkey.io/commands/zrevrank for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose rank is to be retrieved. + + Command response: + Optional[List[Union[int, float]]]: A list containing the rank (as `int`) and score (as `float`) of `member` + in the sorted set, where ranks are ordered from high to low based on scores. + If `key` doesn't exist, or if `member` is not present in the set, `None` will be returned. + + Since: Valkey version 7.2.0. + """ + return self.append_command(RequestType.ZRevRank, [key, member, "WITHSCORE"]) + + def zrem( + self: TTransaction, + key: TEncodable, + members: List[TEncodable], + ) -> TTransaction: + """ + Removes the specified members from the sorted set stored at `key`. + Specified members that are not a member of this set are ignored. + + See https://valkey.io/commands/zrem/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): A list of members to remove from the sorted set. + + Commands response: + int: The number of members that were removed from the sorted set, not including non-existing members. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + """ + return self.append_command(RequestType.ZRem, [key] + members) + + def zremrangebyscore( + self: TTransaction, + key: TEncodable, + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], + ) -> TTransaction: + """ + Removes all elements in the sorted set stored at `key` with a score between `min_score` and `max_score`. + + See https://valkey.io/commands/zremrangebyscore/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to remove from. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to remove up to. + Can be an instance of InfBound representing positive/negative infinity, + or ScoreBoundary representing a specific score and inclusivity. + + Commands response: + int: The number of members that were removed from the sorted set. + If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. + If `min_score` is greater than `max_score`, 0 is returned. + """ + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value + ) + return self.append_command( + RequestType.ZRemRangeByScore, [key, score_min, score_max] + ) + + def zremrangebylex( + self: TTransaction, + key: TEncodable, + min_lex: Union[InfBound, LexBoundary], + max_lex: Union[InfBound, LexBoundary], + ) -> TTransaction: + """ + Removes all elements in the sorted set stored at `key` with a lexicographical order between `min_lex` and + `max_lex`. + + See https://valkey.io/commands/zremrangebylex/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_lex (Union[InfBound, LexBoundary]): The minimum bound of the lexicographical range. + Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` + representing a specific lex and inclusivity. + max_lex (Union[InfBound, LexBoundary]): The maximum bound of the lexicographical range. + Can be an instance of `InfBound` representing positive/negative infinity, or `LexBoundary` + representing a specific lex and inclusivity. + + Command response: + int: The number of members that were removed from the sorted set. + If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. + If `min_lex` is greater than `max_lex`, `0` is returned. + """ + min_lex_arg = ( + min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value + ) + max_lex_arg = ( + max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value + ) + + return self.append_command( + RequestType.ZRemRangeByLex, [key, min_lex_arg, max_lex_arg] + ) + + def zremrangebyrank( + self: TTransaction, + key: TEncodable, + start: int, + end: int, + ) -> TTransaction: + """ + Removes all elements in the sorted set stored at `key` with rank between `start` and `end`. + Both `start` and `end` are zero-based indexes with 0 being the element with the lowest score. + These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. + + See https://valkey.io/commands/zremrangebyrank/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + start (int): The starting point of the range. + end (int): The end of the range. + + Command response: + int: The number of elements that were removed. + If `start` exceeds the end of the sorted set, or if `start` is greater than `end`, `0` is returned. + If `end` exceeds the actual end of the sorted set, the range will stop at the actual end of the sorted set. + If `key` does not exist, `0` is returned. + """ + return self.append_command( + RequestType.ZRemRangeByRank, [key, str(start), str(end)] + ) + + def zlexcount( + self: TTransaction, + key: TEncodable, + min_lex: Union[InfBound, LexBoundary], + max_lex: Union[InfBound, LexBoundary], + ) -> TTransaction: + """ + Returns the number of members in the sorted set stored at `key` with lexographical values between `min_lex` and `max_lex`. + + See https://valkey.io/commands/zlexcount/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + min_lex (Union[InfBound, LexBoundary]): The minimum lexicographical value to count from. + Can be an instance of InfBound representing positive/negative infinity, + or LexBoundary representing a specific lexicographical value and inclusivity. + max_lex (Union[InfBound, LexBoundary]): The maximum lexicographical value to count up to. + Can be an instance of InfBound representing positive/negative infinity, + or LexBoundary representing a specific lexicographical value and inclusivity. + + Command response: + int: The number of members in the specified lexicographical range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns `0`. + If `max_lex < min_lex`, `0` is returned. + """ + min_lex_arg = ( + min_lex.value["lex_arg"] if type(min_lex) == InfBound else min_lex.value + ) + max_lex_arg = ( + max_lex.value["lex_arg"] if type(max_lex) == InfBound else max_lex.value + ) + + return self.append_command( + RequestType.ZLexCount, [key, min_lex_arg, max_lex_arg] + ) + + def zscore(self: TTransaction, key: TEncodable, member: TEncodable) -> TTransaction: + """ + Returns the score of `member` in the sorted set stored at `key`. + + See https://valkey.io/commands/zscore/ for more details. + + Args: + key (TEncodable): The key of the sorted set. + member (TEncodable): The member whose score is to be retrieved. + + Commands response: + Optional[float]: The score of the member. + If `member` does not exist in the sorted set, None is returned. + If `key` does not exist, None is returned. + """ + return self.append_command(RequestType.ZScore, [key, member]) + + def zmscore( + self: TTransaction, key: TEncodable, members: List[TEncodable] + ) -> TTransaction: + """ + Returns the scores associated with the specified `members` in the sorted set stored at `key`. + + See https://valkey.io/commands/zmscore for more details. + + Args: + key (TEncodable): The key of the sorted set. + members (List[TEncodable]): A list of members in the sorted set. + + Command response: + List[Optional[float]]: A list of scores corresponding to `members`. + If a member does not exist in the sorted set, the corresponding value in the list will be None. + """ + return self.append_command(RequestType.ZMScore, [key] + members) + + def zdiff(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Returns the difference between the first sorted set and all the successive sorted sets. + To get the elements with their scores, see `zdiff_withscores`. + + See https://valkey.io/commands/zdiff for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Command response: + List[bytes]: A list of elements representing the difference between the sorted sets. + If the first key does not exist, it is treated as an empty sorted set, and the command returns an + empty list. + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + return self.append_command(RequestType.ZDiff, args) + + def zdiff_withscores(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Returns the difference between the first sorted set and all the successive sorted sets, with the associated scores. + + See https://valkey.io/commands/zdiff for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Command response: + Mapping[bytes, float]: A mapping of elements and their scores representing the difference between the sorted sets. + If the first `key` does not exist, it is treated as an empty sorted set, and the command returns an + empty list. + """ + return self.append_command( + RequestType.ZDiff, [str(len(keys))] + keys + ["WITHSCORES"] + ) + + def zdiffstore( + self: TTransaction, + destination: TEncodable, + keys: List[TEncodable], + ) -> TTransaction: + """ + Calculates the difference between the first sorted set and all the successive sorted sets at `keys` and stores + the difference as a sorted set to `destination`, overwriting it if it already exists. Non-existent keys are + treated as empty sets. + + See https://valkey.io/commands/zdiffstore for more details. + + Args: + destination (TEncodable): The key for the resulting sorted set. + keys (List[TEncodable]): The keys of the sorted sets to compare. + + Command response: + int: The number of members in the resulting sorted set stored at `destination`. + """ + return self.append_command( + RequestType.ZDiffStore, [destination, str(len(keys))] + keys + ) + + def zinter( + self: TTransaction, + keys: List[TEncodable], + ) -> TTransaction: + """ + Computes the intersection of sorted sets given by the specified `keys` and returns a list of intersecting elements. + + See https://valkey.io/commands/zinter/ for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Command response: + List[bytes]: The resulting array of intersecting elements. + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + return self.append_command(RequestType.ZInter, args) + + def zinter_withscores( + self: TTransaction, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> TTransaction: + """ + Computes the intersection of sorted sets given by the specified `keys` and returns a sorted set of intersecting elements with scores. + + See https://valkey.io/commands/zinter/ for more details. + + Args: + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Command response: + Mapping[bytes, float]: The resulting sorted set with scores. + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type) + args.append("WITHSCORES") + return self.append_command(RequestType.ZInter, args) + + def zinterstore( + self: TTransaction, + destination: TEncodable, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> TTransaction: + """ + Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. + If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + + When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + + See https://valkey.io/commands/zinterstore/ for more details. + + Args: + destination (TEncodable): The key of the destination sorted set. + keys (Union[List[TEncodable], Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Command response: + int: The number of elements in the resulting sorted set stored at `destination`. + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type, destination) + return self.append_command(RequestType.ZInterStore, args) + + def zunion( + self: TTransaction, + keys: List[TEncodable], + ) -> TTransaction: + """ + Computes the union of sorted sets given by the specified `keys` and returns a list of union elements. + + See https://valkey.io/commands/zunion/ for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + + Command response: + List[bytes]: The resulting array of union elements. + """ + args: List[TEncodable] = [str(len(keys))] + args.extend(keys) + return self.append_command(RequestType.ZUnion, args) + + def zunion_withscores( + self: TTransaction, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[AggregationType] = None, + ) -> TTransaction: + """ + Computes the union of sorted sets given by the specified `keys` and returns a sorted set of union elements with scores. + + See https://valkey.io/commands/zunion/ for more details. + + Args: + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Command response: + Mapping[bytes, float]: The resulting sorted set with scores. + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type) + args.append("WITHSCORES") + return self.append_command(RequestType.ZUnion, args) + + def zunionstore( + self: TTransaction, + destination: TEncodable, + keys: Union[List[TEncodable], List[Tuple[TEncodable, float]]], + aggregation_type: Optional[Optional[AggregationType]] = None, + ) -> TTransaction: + """ + Computes the union of sorted sets given by the specified `keys` and stores the result in `destination`. + If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + + When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + + see https://valkey.io/commands/zunionstore/ for more details. + + Args: + destination (TEncodable): The key of the destination sorted set. + keys (Union[List[TEncodable], List[Tuple[TEncodable, float]]]): The keys of the sorted sets with possible formats: + List[TEncodable] - for keys only. + List[Tuple[TEncodable, float]] - for weighted keys with score multipliers. + aggregation_type (Optional[AggregationType]): Specifies the aggregation strategy to apply + when combining the scores of elements. See `AggregationType`. + + Command response: + int: The number of elements in the resulting sorted set stored at `destination`. + """ + args = _create_zinter_zunion_cmd_args(keys, aggregation_type, destination) + return self.append_command(RequestType.ZUnionStore, args) + + def zrandmember(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns a random member from the sorted set stored at 'key'. + + See https://valkey.io/commands/zrandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + + Command response: + Optional[bytes]: A random member from the sorted set. + If the sorted set does not exist or is empty, the response will be None. + """ + return self.append_command(RequestType.ZRandMember, [key]) + + def zrandmember_count( + self: TTransaction, key: TEncodable, count: int + ) -> TTransaction: + """ + Retrieves up to the absolute value of `count` random members from the sorted set stored at 'key'. + + See https://valkey.io/commands/zrandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (int): The number of members to return. + If `count` is positive, returns unique members. + If `count` is negative, allows for duplicates members. + + Command response: + List[bytes]: A list of members from the sorted set. + If the sorted set does not exist or is empty, the response will be an empty list. + """ + return self.append_command(RequestType.ZRandMember, [key, str(count)]) + + def zrandmember_withscores( + self: TTransaction, key: TEncodable, count: int + ) -> TTransaction: + """ + Retrieves up to the absolute value of `count` random members along with their scores from the sorted set + stored at 'key'. + + See https://valkey.io/commands/zrandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (int): The number of members to return. + If `count` is positive, returns unique members. + If `count` is negative, allows for duplicates members. + + Command response: + List[List[Union[bytes, float]]]: A list of `[member, score]` lists, where `member` is a random member from + the sorted set and `score` is the associated score. + If the sorted set does not exist or is empty, the response will be an empty list. + """ + return self.append_command( + RequestType.ZRandMember, [key, str(count), "WITHSCORES"] + ) + + def zmpop( + self: TTransaction, + keys: List[TEncodable], + filter: ScoreFilter, + count: Optional[int] = None, + ) -> TTransaction: + """ + Pops a member-score pair from the first non-empty sorted set, with the given keys being checked in the order + that they are given. The optional `count` argument can be used to specify the number of elements to pop, and is + set to 1 by default. The number of popped elements is the minimum from the sorted set's cardinality and `count`. + + See https://valkey.io/commands/zmpop for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + modifier (ScoreFilter): The element pop criteria - either ScoreFilter.MIN or ScoreFilter.MAX to pop + members with the lowest/highest scores accordingly. + count (Optional[int]): The number of elements to pop. + + Command response: + Optional[List[Union[bytes, Mapping[bytes, float]]]]: A two-element list containing the key name of the set from + which elements were popped, and a member-score mapping of the popped elements. If no members could be + popped, returns None. + + Since: Valkey version 7.0.0. + """ + args = [str(len(keys))] + keys + [filter.value] + if count is not None: + args = args + ["COUNT", str(count)] + + return self.append_command(RequestType.ZMPop, args) + + def bzmpop( + self: TTransaction, + keys: List[TEncodable], + modifier: ScoreFilter, + timeout: float, + count: Optional[int] = None, + ) -> TTransaction: + """ + Pops a member-score pair from the first non-empty sorted set, with the given keys being checked in the order + that they are given. Blocks the connection when there are no members to pop from any of the given sorted sets. + + The optional `count` argument can be used to specify the number of elements to pop, and is set to 1 by default. + + The number of popped elements is the minimum from the sorted set's cardinality and `count`. + + `BZMPOP` is the blocking variant of `ZMPOP`. + + See https://valkey.io/commands/bzmpop for more details. + + Note: + `BZMPOP` is a client blocking command, see https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands for more details and best practices. + + Args: + keys (List[TEncodable]): The keys of the sorted sets. + modifier (ScoreFilter): The element pop criteria - either ScoreFilter.MIN or ScoreFilter.MAX to pop + members with the lowest/highest scores accordingly. + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of 0 will + block indefinitely. + count (Optional[int]): The number of elements to pop. + + Command response: + Optional[List[Union[bytes, Mapping[bytes, float]]]]: A two-element list containing the key name of the set from + which elements were popped, and a member-score mapping. If no members could be popped and the timeout + expired, returns None. + + Since: Valkey version 7.0.0. + """ + args = [str(timeout), str(len(keys))] + keys + [modifier.value] + if count is not None: + args = args + ["COUNT", str(count)] + + return self.append_command(RequestType.BZMPop, args) + + def zintercard( + self: TTransaction, keys: List[TEncodable], limit: Optional[int] = None + ) -> TTransaction: + """ + Returns the cardinality of the intersection of the sorted sets specified by `keys`. When provided with the + optional `limit` argument, if the intersection cardinality reaches `limit` partway through the computation, the + algorithm will exit early and yield `limit` as the cardinality. + + See https://valkey.io/commands/zintercard for more details. + + Args: + keys (List[TEncodable]): The keys of the sorted sets to intersect. + limit (Optional[int]): An optional argument that can be used to specify a maximum number for the + intersection cardinality. If limit is not supplied, or if it is set to 0, there will be no limit. + + Command response: + int: The cardinality of the intersection of the given sorted sets, or the `limit` if reached. + + Since: Valkey version 7.0.0. + """ + args = [str(len(keys))] + keys + if limit is not None: + args.extend(["LIMIT", str(limit)]) + + return self.append_command(RequestType.ZInterCard, args) + + def dbsize(self: TTransaction) -> TTransaction: + """ + Returns the number of keys in the currently selected database. + See https://valkey.io/commands/dbsize for more details. + + Commands response: + int: The number of keys in the database. + """ + return self.append_command(RequestType.DBSize, []) + + def pfadd( + self: TTransaction, key: TEncodable, elements: List[TEncodable] + ) -> TTransaction: + """ + Adds all elements to the HyperLogLog data structure stored at the specified `key`. + Creates a new structure if the `key` does not exist. + When no elements are provided, and `key` exists and is a HyperLogLog, then no operation is performed. + + See https://valkey.io/commands/pfadd/ for more details. + + Args: + key (TEncodable): The key of the HyperLogLog data structure to add elements into. + elements (List[TEncodable]): A list of members to add to the HyperLogLog stored at `key`. + + Commands response: + int: If the HyperLogLog is newly created, or if the HyperLogLog approximated cardinality is + altered, then returns 1. Otherwise, returns 0. + """ + return self.append_command(RequestType.PfAdd, [key] + elements) + + def pfcount(self: TTransaction, keys: List[TEncodable]) -> TTransaction: + """ + Estimates the cardinality of the data stored in a HyperLogLog structure for a single key or + calculates the combined cardinality of multiple keys by merging their HyperLogLogs temporarily. + + See https://valkey.io/commands/pfcount for more details. + + Args: + keys (List[TEncodable]): The keys of the HyperLogLog data structures to be analyzed. + + Command response: + int: The approximated cardinality of given HyperLogLog data structures. + The cardinality of a key that does not exist is 0. + """ + return self.append_command(RequestType.PfCount, keys) + + def pfmerge( + self: TTransaction, + destination: TEncodable, + source_keys: List[TEncodable], + ) -> TTransaction: + """ + Merges multiple HyperLogLog values into a unique value. If the destination variable exists, it is treated as one + of the source HyperLogLog data sets, otherwise a new HyperLogLog is created. + + See https://valkey.io/commands/pfmerge for more details. + + Args: + destination (TEncodable): The key of the destination HyperLogLog where the merged data sets will be stored. + source_keys (List[TEncodable]): The keys of the HyperLogLog structures to be merged. + + Command response: + OK: A simple OK response. + """ + return self.append_command(RequestType.PfMerge, [destination] + source_keys) + + def bitcount( + self: TTransaction, + key: TEncodable, + options: Optional[OffsetOptions] = None, + ) -> TTransaction: + """ + Counts the number of set bits (population counting) in a string stored at `key`. The `options` argument can + optionally be provided to count the number of bits in a specific string interval. + + See https://valkey.io/commands/bitcount for more details. + + Args: + key (TEncodable): The key for the string to count the set bits of. + options (Optional[OffsetOptions]): The offset options. + + Command response: + int: If `options` is provided, returns the number of set bits in the string interval specified by `options`. + If `options` is not provided, returns the number of set bits in the string stored at `key`. + Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. + """ + args: List[TEncodable] = [key] + if options is not None: + args.extend(options.to_args()) + + return self.append_command(RequestType.BitCount, args) + + def setbit( + self: TTransaction, key: TEncodable, offset: int, value: int + ) -> TTransaction: + """ + Sets or clears the bit at `offset` in the string value stored at `key`. The `offset` is a zero-based index, + with `0` being the first element of the list, `1` being the next element, and so on. The `offset` must be less + than `2^32` and greater than or equal to `0`. If a key is non-existent then the bit at `offset` is set to + `value` and the preceding bits are set to `0`. + + See https://valkey.io/commands/setbit for more details. + + Args: + key (TEncodable): The key of the string. + offset (int): The index of the bit to be set. + value (int): The bit value to set at `offset`. The value must be `0` or `1`. + + Command response: + int: The bit value that was previously stored at `offset`. + """ + return self.append_command(RequestType.SetBit, [key, str(offset), str(value)]) + + def getbit(self: TTransaction, key: TEncodable, offset: int) -> TTransaction: + """ + Returns the bit value at `offset` in the string value stored at `key`. + `offset` should be greater than or equal to zero. + + See https://valkey.io/commands/getbit for more details. + + Args: + key (TEncodable): The key of the string. + offset (int): The index of the bit to return. + + Command response: + int: The bit at the given `offset` of the string. Returns `0` if the key is empty or if the `offset` exceeds + the length of the string. + """ + return self.append_command(RequestType.GetBit, [key, str(offset)]) + + def bitpos( + self: TTransaction, + key: TEncodable, + bit: int, + start: Optional[int] = None, + ) -> TTransaction: + """ + Returns the position of the first bit matching the given `bit` value. The optional starting offset + `start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on. + The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being + the last byte of the list, `-2` being the penultimate, and so on. + + See https://valkey.io/commands/bitpos for more details. + + Args: + key (TEncodable): The key of the string. + bit (int): The bit value to match. Must be `0` or `1`. + start (Optional[int]): The starting offset. + + Command response: + int: The position of the first occurrence of `bit` in the binary value of the string held at `key`. + If `start` was provided, the search begins at the offset indicated by `start`. + """ + args = [key, str(bit)] if start is None else [key, str(bit), str(start)] + return self.append_command(RequestType.BitPos, args) + + def bitpos_interval( + self: TTransaction, + key: TEncodable, + bit: int, + start: int, + end: int, + index_type: Optional[BitmapIndexType] = None, + ) -> TTransaction: + """ + Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with + `0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative + numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2` + being the penultimate, and so on. + + If you are using Valkey 7.0.0 or above, the optional `index_type` can also be provided to specify whether the + `start` and `end` offsets specify BIT or BYTE offsets. If `index_type` is not provided, BYTE offsets + are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is + specified, `start=0` and `end=2` means to look at the first three bytes. + + See https://valkey.io/commands/bitpos for more details. + + Args: + key (TEncodable): The key of the string. + bit (int): The bit value to match. Must be `0` or `1`. + start (int): The starting offset. + end (int): The ending offset. + index_type (Optional[BitmapIndexType]): The index offset type. This option can only be specified if you are + using Valkey version 7.0.0 or above. Could be either `BitmapIndexType.BYTE` or `BitmapIndexType.BIT`. + If no index type is provided, the indexes will be assumed to be byte indexes. + + Command response: + int: The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the binary + value of the string held at `key`. + """ + if index_type is not None: + args = [key, str(bit), str(start), str(end), index_type.value] + else: + args = [key, str(bit), str(start), str(end)] + + return self.append_command(RequestType.BitPos, args) + + def bitop( + self: TTransaction, + operation: BitwiseOperation, + destination: TEncodable, + keys: List[TEncodable], + ) -> TTransaction: + """ + Perform a bitwise operation between multiple keys (containing string values) and store the result in the + `destination`. + + See https://valkey.io/commands/bitop for more details. + + Args: + operation (BitwiseOperation): The bitwise operation to perform. + destination (TEncodable): The key that will store the resulting string. + keys (List[TEncodable]): The list of keys to perform the bitwise operation on. + + Command response: + int: The size of the string stored in `destination`. + """ + return self.append_command( + RequestType.BitOp, [operation.value, destination] + keys + ) + + def bitfield( + self: TTransaction, + key: TEncodable, + subcommands: List[BitFieldSubCommands], + ) -> TTransaction: + """ + Reads or modifies the array of bits representing the string that is held at `key` based on the specified + `subcommands`. + + See https://valkey.io/commands/bitfield for more details. + + Args: + key (TEncodable): The key of the string. + subcommands (List[BitFieldSubCommands]): The subcommands to be performed on the binary value of the string + at `key`, which could be any of the following: + - `BitFieldGet` + - `BitFieldSet` + - `BitFieldIncrBy` + - `BitFieldOverflow` + + Command response: + List[Optional[int]]: An array of results from the executed subcommands: + - `BitFieldGet` returns the value in `Offset` or `OffsetMultiplier`. + - `BitFieldSet` returns the old value in `Offset` or `OffsetMultiplier`. + - `BitFieldIncrBy` returns the new value in `Offset` or `OffsetMultiplier`. + - `BitFieldOverflow` determines the behavior of the "SET" and "INCRBY" subcommands when an overflow or + underflow occurs. "OVERFLOW" does not return a value and does not contribute a value to the list + response. + """ + args = [key] + _create_bitfield_args(subcommands) + return self.append_command(RequestType.BitField, args) + + def bitfield_read_only( + self: TTransaction, key: TEncodable, subcommands: List[BitFieldGet] + ) -> TTransaction: + """ + Reads the array of bits representing the string that is held at `key` based on the specified `subcommands`. + + See https://valkey.io/commands/bitfield_ro for more details. + + Args: + key (TEncodable): The key of the string. + subcommands (List[BitFieldGet]): The "GET" subcommands to be performed. + + Command response: + List[int]: An array of results from the "GET" subcommands. + + Since: Valkey version 6.0.0. + """ + args = [key] + _create_bitfield_read_only_args(subcommands) + return self.append_command(RequestType.BitFieldReadOnly, args) + + def object_encoding(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the internal encoding for the Valkey object stored at `key`. + + See https://valkey.io/commands/object-encoding for more details. + + Args: + key (TEncodable): The `key` of the object to get the internal encoding of. + + Command response: + Optional[bytes]: If `key` exists, returns the internal encoding of the object stored at + `key` as a bytes string. Otherwise, returns None. + """ + return self.append_command(RequestType.ObjectEncoding, [key]) + + def object_freq(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the logarithmic access frequency counter of a Valkey object stored at `key`. + + See https://valkey.io/commands/object-freq for more details. + + Args: + key (TEncodable): The key of the object to get the logarithmic access frequency counter of. + + Command response: + Optional[int]: If `key` exists, returns the logarithmic access frequency counter of the object stored at `key` as an + integer. Otherwise, returns None. + """ + return self.append_command(RequestType.ObjectFreq, [key]) + + def object_idletime(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the time in seconds since the last access to the value stored at `key`. + + See https://valkey.io/commands/object-idletime for more details. + + Args: + key (TEncodable): The key of the object to get the idle time of. + + Command response: + Optional[int]: If `key` exists, returns the idle time in seconds. Otherwise, returns None. + """ + return self.append_command(RequestType.ObjectIdleTime, [key]) + + def object_refcount(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns the reference count of the object stored at `key`. + + See https://valkey.io/commands/object-refcount for more details. + + Args: + key (TEncodable): The key of the object to get the reference count of. + + Command response: + Optional[int]: If `key` exists, returns the reference count of the object stored at `key` as an integer. + Otherwise, returns None. + """ + return self.append_command(RequestType.ObjectRefCount, [key]) + + def srandmember(self: TTransaction, key: TEncodable) -> TTransaction: + """ + Returns a random element from the set value stored at 'key'. + + See https://valkey.io/commands/srandmember for more details. + + Args: + key (TEncodable): The key from which to retrieve the set member. + + Command Response: + bytes: A random element from the set, or None if 'key' does not exist. + """ + return self.append_command(RequestType.SRandMember, [key]) + + def srandmember_count( + self: TTransaction, key: TEncodable, count: int + ) -> TTransaction: + """ + Returns one or more random elements from the set value stored at 'key'. + + See https://valkey.io/commands/srandmember for more details. + + Args: + key (TEncodable): The key of the sorted set. + count (int): The number of members to return. + If `count` is positive, returns unique members. + If `count` is negative, allows for duplicates members. + + Command Response: + List[TEncodable]: A list of members from the set. + If the set does not exist or is empty, the response will be an empty list. + """ + return self.append_command(RequestType.SRandMember, [key, str(count)]) + + def flushall( + self: TTransaction, flush_mode: Optional[FlushMode] = None + ) -> TTransaction: + """ + Deletes all the keys of all the existing databases. This command never fails. + See https://valkey.io/commands/flushall for more details. + + Args: + flush_mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + + Command Response: + TOK: OK. + """ + args: List[TEncodable] = [] + if flush_mode is not None: + args.append(flush_mode.value) + return self.append_command(RequestType.FlushAll, args) + + def flushdb( + self: TTransaction, flush_mode: Optional[FlushMode] = None + ) -> TTransaction: + """ + Deletes all the keys of the currently selected database. This command never fails. + + See https://valkey.io/commands/flushdb for more details. + + Args: + flush_mode (Optional[FlushMode]): The flushing mode, could be either `SYNC` or `ASYNC`. + + Command Response: + TOK: OK. + """ + args: List[TEncodable] = [] + if flush_mode is not None: + args.append(flush_mode.value) + return self.append_command(RequestType.FlushDB, args) + + def getex( + self: TTransaction, key: TEncodable, expiry: Optional[ExpiryGetEx] = None + ) -> TTransaction: + """ + Get the value of `key` and optionally set its expiration. GETEX is similar to GET. + See https://valkey.io/commands/getex for more details. + + Args: + key (TEncodable): The key to get. + expiry (Optional[ExpirySet], optional): set expiriation to the given key. + Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `PERSIST`] in the Valkey API. + + Command Response: + Optional[bytes]: + If `key` exists, return the value stored at `key` + If 'key` does not exist, return 'None' + + Since: Valkey version 6.2.0. + """ + args: List[TEncodable] = [key] + if expiry is not None: + args.extend(expiry.get_cmd_args()) + return self.append_command(RequestType.GetEx, args) + + def lolwut( + self: TTransaction, + version: Optional[int] = None, + parameters: Optional[List[int]] = None, + ) -> TTransaction: + """ + Displays a piece of generative computer art and the Valkey version. + + See https://valkey.io/commands/lolwut for more details. + + Args: + version (Optional[int]): Version of computer art to generate. + parameters (Optional[List[int]]): Additional set of arguments in order to change the output: + For version `5`, those are length of the line, number of squares per row, and number of squares per column. + For version `6`, those are number of columns and number of lines. + + Command Response: + bytes: A piece of generative computer art along with the current Valkey version. + """ + args: List[TEncodable] = [] + if version is not None: + args.extend(["VERSION", str(version)]) + if parameters: + for var in parameters: + args.extend(str(var)) + return self.append_command(RequestType.Lolwut, args) + + def random_key(self: TTransaction) -> TTransaction: + """ + Returns a random existing key name. + + See https://valkey.io/commands/randomkey for more details. + + Command response: + Optional[bytes]: A random existing key name. + """ + return self.append_command(RequestType.RandomKey, []) + + def sscan( + self: TTransaction, + key: TEncodable, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + ) -> TTransaction: + """ + Iterates incrementally over a set. + + See https://valkey.io/commands/sscan for more details. + + Args: + key (TEncodable): The key of the set. + cursor (TEncodable): The cursor that points to the next iteration of results. A value of "0" indicates the start of + the search. + match (Optional[TEncodable]): The match filter is applied to the result of the command and will only include + strings or bytes strings that match the pattern specified. If the set is large enough for scan commands to return only a + subset of the set then there could be a case where the result is empty although there are items that + match the pattern specified. This is due to the default `COUNT` being `10` which indicates that it will + only fetch and match `10` items from the list. + count (Optional[int]): `COUNT` is a just a hint for the command for how many elements to fetch from the set. + `COUNT` could be ignored until the set is large enough for the `SCAN` commands to represent the results + as compact single-allocation packed encoding. + + Command Response: + List[Union[bytes, List[bytes]]]: An `Array` of the `cursor` and the subset of the set held by `key`. + The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + returned on the last iteration of the set. The second element is always an `Array` of the subset of the + set held in `key`. + """ + args = [key, cursor] + if match is not None: + args += ["MATCH", match] + if count is not None: + args += ["COUNT", str(count)] + + return self.append_command(RequestType.SScan, args) + + def zscan( + self: TTransaction, + key: TEncodable, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + ) -> TTransaction: + """ + Iterates incrementally over a sorted set. + + See https://valkey.io/commands/zscan for more details. + + Args: + key (TEncodable): The key of the sorted set. + cursor (TEncodable): The cursor that points to the next iteration of results. A value of "0" indicates the start of + the search. + match (Optional[TEncodable]): The match filter is applied to the result of the command and will only include + strings or byte string that match the pattern specified. If the sorted set is large enough for scan commands to return + only a subset of the sorted set then there could be a case where the result is empty although there are + items that match the pattern specified. This is due to the default `COUNT` being `10` which indicates + that it will only fetch and match `10` items from the list. + count (Optional[int]): `COUNT` is a just a hint for the command for how many elements to fetch from the + sorted set. `COUNT` could be ignored until the sorted set is large enough for the `SCAN` commands to + represent the results as compact single-allocation packed encoding. + + Returns: + List[Union[bytes, List[bytes]]]: An `Array` of the `cursor` and the subset of the sorted set held by `key`. + The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + returned on the last iteration of the sorted set. The second element is always an `Array` of the subset + of the sorted set held in `key`. The `Array` in the second element is always a flattened series of + `String` pairs, where the value is at even indices and the score is at odd indices. + """ + args = [key, cursor] + if match is not None: + args += ["MATCH", match] + if count is not None: + args += ["COUNT", str(count)] + + return self.append_command(RequestType.ZScan, args) + + def hscan( + self: TTransaction, + key: TEncodable, + cursor: TEncodable, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + ) -> TTransaction: + """ + Iterates incrementally over a hash. + + See https://valkey.io/commands/hscan for more details. + + Args: + key (TEncodable): The key of the set. + cursor (TEncodable): The cursor that points to the next iteration of results. A value of "0" indicates the start of + the search. + match (Optional[TEncodable]): The match filter is applied to the result of the command and will only include + strings or bytes strings that match the pattern specified. If the hash is large enough for scan commands to return only a + subset of the hash then there could be a case where the result is empty although there are items that + match the pattern specified. This is due to the default `COUNT` being `10` which indicates that it will + only fetch and match `10` items from the list. + count (Optional[int]): `COUNT` is a just a hint for the command for how many elements to fetch from the hash. + `COUNT` could be ignored until the hash is large enough for the `SCAN` commands to represent the results + as compact single-allocation packed encoding. + + Returns: + List[Union[bytes, List[bytes]]]: An `Array` of the `cursor` and the subset of the hash held by `key`. + The first element is always the `cursor` for the next iteration of results. `0` will be the `cursor` + returned on the last iteration of the hash. The second element is always an `Array` of the subset of the + hash held in `key`. The `Array` in the second element is always a flattened series of `String` pairs, + where the value is at even indices and the score is at odd indices. + """ + args = [key, cursor] + if match is not None: + args += ["MATCH", match] + if count is not None: + args += ["COUNT", str(count)] + + return self.append_command(RequestType.HScan, args) + + def lcs( + self: TTransaction, + key1: TEncodable, + key2: TEncodable, + ) -> TTransaction: + """ + Returns the longest common subsequence between strings stored at key1 and key2. + + Note that this is different than the longest common string algorithm, since + matching characters in the two strings do not need to be contiguous. + + For instance the LCS between "foo" and "fao" is "fo", since scanning the two strings + from left to right, the longest common set of characters is composed of the first "f" and then the "o". + + See https://valkey.io/commands/lcs for more details. + + Args: + key1 (TEncodable): The key that stores the first value. + key2 (TEncodable): The key that stores the second value. + + Command Response: + A Bytes String containing the longest common subsequence between the 2 strings. + An empty Bytes String is returned if the keys do not exist or have no common subsequences. + + Since: Valkey version 7.0.0. + """ + args = [key1, key2] + + return self.append_command(RequestType.LCS, args) + + def lcs_len( + self: TTransaction, + key1: TEncodable, + key2: TEncodable, + ) -> TTransaction: + """ + Returns the length of the longest common subsequence between strings stored at key1 and key2. + + Note that this is different than the longest common string algorithm, since + matching characters in the two strings do not need to be contiguous. + + For instance the LCS between "foo" and "fao" is "fo", since scanning the two strings + from left to right, the longest common set of characters is composed of the first "f" and then the "o". + + See https://valkey.io/commands/lcs for more details. + + Args: + key1 (TEncodable): The key that stores the first value. + key2 (TEncodable): The key that stores the second value. + + Command Response: + The length of the longest common subsequence between the 2 strings. + + Since: Valkey version 7.0.0. + """ + args = [key1, key2, "LEN"] + + return self.append_command(RequestType.LCS, args) + + def lcs_idx( + self: TTransaction, + key1: TEncodable, + key2: TEncodable, + min_match_len: Optional[int] = None, + with_match_len: Optional[bool] = False, + ) -> TTransaction: + """ + Returns the indices and length of the longest common subsequence between strings stored at key1 and key2. + + Note that this is different than the longest common string algorithm, since + matching characters in the two strings do not need to be contiguous. + + For instance the LCS between "foo" and "fao" is "fo", since scanning the two strings + from left to right, the longest common set of characters is composed of the first "f" and then the "o". + + See https://valkey.io/commands/lcs for more details. + + Args: + key1 (TEncodable): The key that stores the first value. + key2 (TEncodable): The key that stores the second value. + min_match_len (Optional[int]): The minimum length of matches to include in the result. + with_match_len (Optional[bool]): If True, include the length of the substring matched for each substring. + + Command Response: + A Map containing the indices of the longest common subsequence between the + 2 strings and the length of the longest common subsequence. The resulting map contains two + keys, "matches" and "len": + - "len" is mapped to the length of the longest common subsequence between the 2 strings. + - "matches" is mapped to a three dimensional int array that stores pairs of indices that + represent the location of the common subsequences in the strings held by key1 and key2, + with the length of the match after each matches, if with_match_len is enabled. + + Since: Valkey version 7.0.0. + """ + args = [key1, key2, "IDX"] + + if min_match_len is not None: + args.extend(["MINMATCHLEN", str(min_match_len)]) + + if with_match_len: + args.append("WITHMATCHLEN") + + return self.append_command(RequestType.LCS, args) + + def wait( + self: TTransaction, + numreplicas: int, + timeout: int, + ) -> TTransaction: + """ + Returns the number of replicas that acknowledged the write commands sent by the current client + before this command, both in the case where the specified number of replicas are reached, or + when the timeout is reached. + + See https://valkey.io/commands/wait for more details. + + Args: + numreplicas (int): The number of replicas to reach. + timeout (int): The timeout value specified in milliseconds. + + Command Response: + bytes: The number of replicas reached by all the writes performed in the context of the current connection. + """ + args: List[TEncodable] = [str(numreplicas), str(timeout)] + return self.append_command(RequestType.Wait, args) + + def lpos( + self: TTransaction, + key: TEncodable, + element: TEncodable, + rank: Optional[int] = None, + count: Optional[int] = None, + max_len: Optional[int] = None, + ) -> TTransaction: + """ + Returns the index or indexes of element(s) matching `element` in the `key` list. If no match is found, + None is returned. + + See https://valkey.io/commands/lpos for more details. + + Args: + key (TEncodable): The name of the list. + element (TEncodable): The value to search for within the list. + rank (Optional[int]): The rank of the match to return. + count (Optional[int]): The number of matches wanted. A `count` of 0 returns all the matches. + max_len (Optional[int]): The maximum number of comparisons to make between the element and the items + in the list. A `max_len` of 0 means unlimited amount of comparisons. + + Command Response: + Union[int, List[int], None]: The index of the first occurrence of `element`, + or None if `element` is not in the list. + With the `count` option, a list of indices of matching elements will be returned. + + Since: Valkey version 6.0.6. + """ + args = [key, element] + + if rank is not None: + args.extend(["RANK", str(rank)]) + + if count is not None: + args.extend(["COUNT", str(count)]) + + if max_len is not None: + args.extend(["MAXLEN", str(max_len)]) + + return self.append_command(RequestType.LPos, args) + + def xclaim( + self: TTransaction, + key: TEncodable, + group: TEncodable, + consumer: TEncodable, + min_idle_time_ms: int, + ids: List[TEncodable], + options: Optional[StreamClaimOptions] = None, + ) -> TTransaction: + """ + Changes the ownership of a pending message. + + See https://valkey.io/commands/xclaim for more details. + + Args: + key (TEncodable): The key of the stream. + group (TEncodable): The consumer group name. + consumer (TEncodable): The group consumer. + min_idle_time_ms (int): The minimum idle time for the message to be claimed. + ids (List[TEncodable]): A array of entry ids. + options (Optional[StreamClaimOptions]): Stream claim options. + + Returns: + A Mapping of message entries with the format + {"entryId": [["entry", "data"], ...], ...} that are claimed by the consumer. + """ + + args = [key, group, consumer, str(min_idle_time_ms), *ids] + + if options: + args.extend(options.to_args()) + + return self.append_command(RequestType.XClaim, args) + + def xclaim_just_id( + self: TTransaction, + key: TEncodable, + group: TEncodable, + consumer: TEncodable, + min_idle_time_ms: int, + ids: List[TEncodable], + options: Optional[StreamClaimOptions] = None, + ) -> TTransaction: + """ + Changes the ownership of a pending message. This function returns a List with + only the message/entry IDs, and is equivalent to using JUSTID in the Valkey API. + + See https://valkey.io/commands/xclaim for more details. + + Args: + key (TEncodable): The key of the stream. + group (TEncodable): The consumer group name. + consumer (TEncodable): The group consumer. + min_idle_time_ms (int): The minimum idle time for the message to be claimed. + ids (List[TEncodable]): A array of entry ids. + options (Optional[StreamClaimOptions]): Stream claim options. + + Returns: + A List of message ids claimed by the consumer. + """ + + args = [ + key, + group, + consumer, + str(min_idle_time_ms), + *ids, + StreamClaimOptions.JUST_ID_VALKEY_API, + ] + + if options: + args.extend(options.to_args()) + + return self.append_command(RequestType.XClaim, args) + + +class Transaction(BaseTransaction): """ + Extends BaseTransaction class for standalone commands that are not supported in cluster mode. + + Command Response: + The response for each command depends on the executed command. Specific response types + are documented alongside each method. + + Example: + transaction = Transaction() + >>> transaction.set("key", "value") + >>> transaction.select(1) # Standalone command + >>> transaction.get("key") + >>> await client.exec(transaction) + [OK , OK , None] + + """ + + # TODO: add SLAVEOF and all SENTINEL commands + def move(self, key: TEncodable, db_index: int) -> "Transaction": + """ + Move `key` from the currently selected database to the database specified by `db_index`. + + See https://valkey.io/commands/move/ for more details. + + Args: + key (TEncodable): The key to move. + db_index (int): The index of the database to move `key` to. + + Commands response: + bool: True if `key` was moved, or False if the `key` already exists in the destination database + or does not exist in the source database. + """ + return self.append_command(RequestType.Move, [key, str(db_index)]) + + def select(self, index: int) -> "Transaction": + """ + Change the currently selected database. + See https://valkey.io/commands/select/ for details. + + Args: + index (int): The index of the database to select. + + Command response: + A simple OK response. + """ + return self.append_command(RequestType.Select, [str(index)]) + + def sort( + self, + key: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> "Transaction": + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + To store the result into a new key, see `sort_store`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + by_pattern (Optional[TEncodable]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[TEncodable]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + List[Optional[bytes]]: Returns a list of sorted elements. + """ + args = _build_sort_args(key, by_pattern, limit, get_patterns, order, alpha) + return self.append_command(RequestType.Sort, args) + + def sort_ro( + self, + key: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> "Transaction": + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort_ro` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + This command is routed depending on the client's `ReadFrom` strategy. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + by_pattern (Optional[TEncodable]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[TEncodable]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + List[Optional[bytes]]: Returns a list of sorted elements. + + Since: Valkey version 7.0.0. + """ + args = _build_sort_args(key, by_pattern, limit, get_patterns, order, alpha) + return self.append_command(RequestType.SortReadOnly, args) + + def sort_store( + self, + key: TEncodable, + destination: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> "Transaction": + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + The `sort` command can be used to sort elements based on different criteria, apply transformations on sorted elements, and store the result in a new key. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + destination (TEncodable): The key where the sorted result will be stored. + by_pattern (Optional[TEncodable]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[TEncodable]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + int: The number of elements in the sorted key stored at `store`. + """ + args = _build_sort_args( + key, by_pattern, limit, get_patterns, order, alpha, store=destination + ) + return self.append_command(RequestType.Sort, args) + + def copy( + self, + source: TEncodable, + destination: TEncodable, + destinationDB: Optional[int] = None, + replace: Optional[bool] = None, + ) -> "Transaction": + """ + Copies the value stored at the `source` to the `destination` key. If `destinationDB` + is specified, the value will be copied to the database specified by `destinationDB`, + otherwise the current database will be used. When `replace` is True, removes the + `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Args: + source (TEncodable): The key to the source value. + destination (TEncodable): The key where the value should be copied to. + destinationDB (Optional[int]): The alternative logical database index for the destination key. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Command response: + bool: True if the source was copied. Otherwise, return False. + + Since: Valkey version 6.2.0. + """ + args = [source, destination] + if destinationDB is not None: + args.extend(["DB", str(destinationDB)]) + if replace is not None: + args.append("REPLACE") + + return self.append_command(RequestType.Copy, args) + + def publish(self, message: TEncodable, channel: TEncodable) -> "Transaction": + """ + Publish a message on pubsub channel. + See https://valkey.io/commands/publish for more details. + + Args: + message (TEncodable): Message to publish + channel (TEncodable): Channel to publish the message on. + + Command Respose: + int: Number of subscriptions in that shard that received the message. + + """ + return self.append_command(RequestType.Publish, [channel, message]) + + +class ClusterTransaction(BaseTransaction): + """ + Extends BaseTransaction class for cluster mode commands that are not supported in standalone. + + Command Response: + The response for each command depends on the executed command. Specific response types + are documented alongside each method. + """ + + def sort( + self, + key: TEncodable, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> "ClusterTransaction": + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + This command is routed to primary only. + To store the result into a new key, see `sort_store`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + List[bytes]: A list of sorted elements. + """ + args = _build_sort_args(key, None, limit, None, order, alpha) + return self.append_command(RequestType.Sort, args) + + def sort_ro( + self, + key: TEncodable, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> "ClusterTransaction": + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort_ro` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + This command is routed depending on the client's `ReadFrom` strategy. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + List[bytes]: A list of sorted elements. + + Since: Valkey version 7.0.0. + """ + args = _build_sort_args(key, None, limit, None, order, alpha) + return self.append_command(RequestType.SortReadOnly, args) + + def sort_store( + self, + key: TEncodable, + destination: TEncodable, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> "ClusterTransaction": + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + When in cluster mode, `key` and `store` must map to the same hash slot. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (TEncodable): The key of the list, set, or sorted set to be sorted. + destination (TEncodable): The key where the sorted result will be stored. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + int: The number of elements in the sorted key stored at `store`. + """ + args = _build_sort_args(key, None, limit, None, order, alpha, store=destination) + return self.append_command(RequestType.Sort, args) + + def copy( + self, + source: TEncodable, + destination: TEncodable, + replace: Optional[bool] = None, + ) -> "ClusterTransaction": + """ + Copies the value stored at the `source` to the `destination` key. When `replace` is True, + removes the `destination` key first if it already exists, otherwise performs no action. + + See https://valkey.io/commands/copy for more details. + + Args: + source (TEncodable): The key to the source value. + destination (TEncodable): The key where the value should be copied to. + replace (Optional[bool]): If the destination key should be removed before copying the value to it. + + Command response: + bool: True if the source was copied. Otherwise, return False. + + Since: Valkey version 6.2.0. + """ + args = [source, destination] + if replace is not None: + args.append("REPLACE") + + return self.append_command(RequestType.Copy, args) + + def publish( + self, message: str, channel: str, sharded: bool = False + ) -> "ClusterTransaction": + """ + Publish a message on pubsub channel. + This command aggregates PUBLISH and SPUBLISH commands functionalities. + The mode is selected using the 'sharded' parameter + See https://valkey.io/commands/publish and https://valkey.io/commands/spublish for more details. + + Args: + message (str): Message to publish + channel (str): Channel to publish the message on. + sharded (bool): Use sharded pubsub mode. Available since Valkey version 7.0. + + Returns: + int: Number of subscriptions in that shard that received the message. + """ + return self.append_command( + RequestType.SPublish if sharded else RequestType.Publish, [channel, message] + ) # TODO: add all CLUSTER commands - pass diff --git a/python/python/glide/config.py b/python/python/glide/config.py index 5c6ba07969..87f6b06087 100644 --- a/python/python/glide/config.py +++ b/python/python/glide/config.py @@ -1,8 +1,13 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from enum import Enum -from typing import List, Optional, Union +from __future__ import annotations +from dataclasses import dataclass +from enum import Enum, IntEnum +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from glide.async_commands.core import CoreCommands +from glide.exceptions import ConfigurationError from glide.protobuf.connection_request_pb2 import ConnectionRequest from glide.protobuf.connection_request_pb2 import ProtocolVersion as SentProtocolVersion from glide.protobuf.connection_request_pb2 import ReadFrom as ProtobufReadFrom @@ -45,11 +50,11 @@ class ProtocolVersion(Enum): RESP2 = SentProtocolVersion.RESP2 """ - Communicate using Redis RESP2. + Communicate using RESP2. """ RESP3 = SentProtocolVersion.RESP3 """ - Communicate using Redis RESP3. + Communicate using RESP3. """ @@ -74,18 +79,18 @@ def __init__(self, num_of_retries: int, factor: int, exponent_base: int): self.exponent_base = exponent_base -class RedisCredentials: +class ServerCredentials: def __init__( self, password: str, username: Optional[str] = None, ): """ - Represents the credentials for connecting to a Redis server. + Represents the credentials for connecting to a server. Args: - password (str): The password that will be used for authenticating connections to the Redis servers. - username (Optional[str]): The username that will be used for authenticating connections to the Redis servers. + password (str): The password that will be used for authenticating connections to the servers. + username (Optional[str]): The username that will be used for authenticating connections to the servers. If not supplied, "default" will be used. """ self.password = password @@ -124,14 +129,14 @@ def __init__( self, addresses: List[NodeAddress], use_tls: bool = False, - credentials: Optional[RedisCredentials] = None, + credentials: Optional[ServerCredentials] = None, read_from: ReadFrom = ReadFrom.PRIMARY, request_timeout: Optional[int] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, ): """ - Represents the configuration settings for a Redis client. + Represents the configuration settings for a Glide client. Args: addresses (List[NodeAddress]): DNS Addresses and ports of known nodes in the cluster. @@ -146,7 +151,7 @@ def __init__( ]. use_tls (bool): True if communication with the cluster should use Transport Level Security. Should match the TLS configuration of the server/cluster, otherwise the connection attempt will fail - credentials (RedisCredentials): Credentials for authentication process. + credentials (ServerCredentials): Credentials for authentication process. If none are set, the client will not authenticate itself with the server. read_from (ReadFrom): If not set, `PRIMARY` will be used. request_timeout (Optional[int]): The duration in milliseconds that the client should wait for a request to complete. @@ -194,10 +199,18 @@ def _create_a_protobuf_conn_request( return request + def _is_pubsub_configured(self) -> bool: + return False + + def _get_pubsub_callback_and_context( + self, + ) -> Tuple[Optional[Callable[[CoreCommands.PubSubMsg, Any], None]], Any]: + return None, None + -class RedisClientConfiguration(BaseClientConfiguration): +class GlideClientConfiguration(BaseClientConfiguration): """ - Represents the configuration settings for a Standalone Redis client. + Represents the configuration settings for a Standalone Glide client. Args: addresses (List[NodeAddress]): DNS Addresses and ports of known nodes in the cluster. @@ -208,7 +221,7 @@ class RedisClientConfiguration(BaseClientConfiguration): {address: sample-address-0002.use2.cache.amazonaws.com, port:6379} ]. use_tls (bool): True if communication with the cluster should use Transport Level Security. - credentials (RedisCredentials): Credentials for authentication process. + credentials (ServerCredentials): Credentials for authentication process. If none are set, the client will not authenticate itself with the server. read_from (ReadFrom): If not set, `PRIMARY` will be used. request_timeout (Optional[int]): The duration in milliseconds that the client should wait for a request to complete. @@ -220,20 +233,53 @@ class RedisClientConfiguration(BaseClientConfiguration): If not set, a default backoff strategy will be used. database_id (Optional[int]): index of the logical database to connect to. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. - protocol (ProtocolVersion): The version of the Redis RESP protocol to communicate with the server. + protocol (ProtocolVersion): The version of the RESP protocol to communicate with the server. + pubsub_subscriptions (Optional[GlideClientConfiguration.PubSubSubscriptions]): Pubsub subscriptions to be used for the client. + Will be applied via SUBSCRIBE/PSUBSCRIBE commands during connection establishment. """ + class PubSubChannelModes(IntEnum): + """ + Describes pubsub subsciption modes. + See https://valkey.io/docs/topics/pubsub/ for more details + """ + + Exact = 0 + """ Use exact channel names """ + Pattern = 1 + """ Use channel name patterns """ + + @dataclass + class PubSubSubscriptions: + """Describes pubsub configuration for standalone mode client. + + Attributes: + channels_and_patterns (Dict[GlideClientConfiguration.PubSubChannelModes, Set[str]]): + Channels and patterns by modes. + callback (Optional[Callable[[CoreCommands.PubSubMsg, Any], None]]): + Optional callback to accept the incomming messages. + context (Any): + Arbitrary context to pass to the callback. + """ + + channels_and_patterns: Dict[ + GlideClientConfiguration.PubSubChannelModes, Set[str] + ] + callback: Optional[Callable[[CoreCommands.PubSubMsg, Any], None]] + context: Any + def __init__( self, addresses: List[NodeAddress], use_tls: bool = False, - credentials: Optional[RedisCredentials] = None, + credentials: Optional[ServerCredentials] = None, read_from: ReadFrom = ReadFrom.PRIMARY, request_timeout: Optional[int] = None, reconnect_strategy: Optional[BackoffStrategy] = None, database_id: Optional[int] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, + pubsub_subscriptions: Optional[PubSubSubscriptions] = None, ): super().__init__( addresses=addresses, @@ -246,6 +292,7 @@ def __init__( ) self.reconnect_strategy = reconnect_strategy self.database_id = database_id + self.pubsub_subscriptions = pubsub_subscriptions def _create_a_protobuf_conn_request( self, cluster_mode: bool = False @@ -263,12 +310,44 @@ def _create_a_protobuf_conn_request( if self.database_id: request.database_id = self.database_id + if self.pubsub_subscriptions: + if self.protocol == ProtocolVersion.RESP2: + raise ConfigurationError( + "PubSub subscriptions require RESP3 protocol, but RESP2 was configured." + ) + if ( + self.pubsub_subscriptions.context is not None + and not self.pubsub_subscriptions.callback + ): + raise ConfigurationError( + "PubSub subscriptions with a context require a callback function to be configured." + ) + for ( + channel_type, + channels_patterns, + ) in self.pubsub_subscriptions.channels_and_patterns.items(): + entry = request.pubsub_subscriptions.channels_or_patterns_by_type[ + int(channel_type) + ] + for channel_pattern in channels_patterns: + entry.channels_or_patterns.append(str.encode(channel_pattern)) + return request + def _is_pubsub_configured(self) -> bool: + return self.pubsub_subscriptions is not None + + def _get_pubsub_callback_and_context( + self, + ) -> Tuple[Optional[Callable[[CoreCommands.PubSubMsg, Any], None]], Any]: + if self.pubsub_subscriptions: + return self.pubsub_subscriptions.callback, self.pubsub_subscriptions.context + return None, None + -class ClusterClientConfiguration(BaseClientConfiguration): +class GlideClusterClientConfiguration(BaseClientConfiguration): """ - Represents the configuration settings for a Cluster Redis client. + Represents the configuration settings for a Cluster Glide client. Args: addresses (List[NodeAddress]): DNS Addresses and ports of known nodes in the cluster. @@ -278,29 +357,63 @@ class ClusterClientConfiguration(BaseClientConfiguration): {address:configuration-endpoint.use1.cache.amazonaws.com, port:6379} ]. use_tls (bool): True if communication with the cluster should use Transport Level Security. - credentials (RedisCredentials): Credentials for authentication process. + credentials (ServerCredentials): Credentials for authentication process. If none are set, the client will not authenticate itself with the server. read_from (ReadFrom): If not set, `PRIMARY` will be used. request_timeout (Optional[int]): The duration in milliseconds that the client should wait for a request to complete. This duration encompasses sending the request, awaiting for a response from the server, and any required reconnections or retries. If the specified timeout is exceeded for a pending request, it will result in a timeout error. If not set, a default value will be used. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. - protocol (ProtocolVersion): The version of the Redis RESP protocol to communicate with the server. + protocol (ProtocolVersion): The version of the RESP protocol to communicate with the server. periodic_checks (Union[PeriodicChecksStatus, PeriodicChecksManualInterval]): Configure the periodic topology checks. These checks evaluate changes in the cluster's topology, triggering a slot refresh when detected. Periodic checks ensure a quick and efficient process by querying a limited number of nodes. Defaults to PeriodicChecksStatus.ENABLED_DEFAULT_CONFIGS. + pubsub_subscriptions (Optional[GlideClusterClientConfiguration.PubSubSubscriptions]): Pubsub subscriptions to be used for the client. + Will be applied via SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE commands during connection establishment. Notes: Currently, the reconnection strategy in cluster mode is not configurable, and exponential backoff with fixed values is used. """ + class PubSubChannelModes(IntEnum): + """ + Describes pubsub subsciption modes. + See https://valkey.io/docs/topics/pubsub/ for more details + """ + + Exact = 0 + """ Use exact channel names """ + Pattern = 1 + """ Use channel name patterns """ + Sharded = 2 + """ Use sharded pubsub. Available since Valkey version 7.0. """ + + @dataclass + class PubSubSubscriptions: + """Describes pubsub configuration for cluster mode client. + + Attributes: + channels_and_patterns (Dict[GlideClusterClientConfiguration.PubSubChannelModes, Set[str]]): + Channels and patterns by modes. + callback (Optional[Callable[[CoreCommands.PubSubMsg, Any], None]]): + Optional callback to accept the incoming messages. + context (Any): + Arbitrary context to pass to the callback. + """ + + channels_and_patterns: Dict[ + GlideClusterClientConfiguration.PubSubChannelModes, Set[str] + ] + callback: Optional[Callable[[CoreCommands.PubSubMsg, Any], None]] + context: Any + def __init__( self, addresses: List[NodeAddress], use_tls: bool = False, - credentials: Optional[RedisCredentials] = None, + credentials: Optional[ServerCredentials] = None, read_from: ReadFrom = ReadFrom.PRIMARY, request_timeout: Optional[int] = None, client_name: Optional[str] = None, @@ -308,6 +421,7 @@ def __init__( periodic_checks: Union[ PeriodicChecksStatus, PeriodicChecksManualInterval ] = PeriodicChecksStatus.ENABLED_DEFAULT_CONFIGS, + pubsub_subscriptions: Optional[PubSubSubscriptions] = None, ): super().__init__( addresses=addresses, @@ -319,6 +433,7 @@ def __init__( protocol=protocol, ) self.periodic_checks = periodic_checks + self.pubsub_subscriptions = pubsub_subscriptions def _create_a_protobuf_conn_request( self, cluster_mode: bool = False @@ -332,4 +447,36 @@ def _create_a_protobuf_conn_request( elif self.periodic_checks == PeriodicChecksStatus.DISABLED: request.periodic_checks_disabled.SetInParent() + if self.pubsub_subscriptions: + if self.protocol == ProtocolVersion.RESP2: + raise ConfigurationError( + "PubSub subscriptions require RESP3 protocol, but RESP2 was configured." + ) + if ( + self.pubsub_subscriptions.context is not None + and not self.pubsub_subscriptions.callback + ): + raise ConfigurationError( + "PubSub subscriptions with a context require a callback function to be configured." + ) + for ( + channel_type, + channels_patterns, + ) in self.pubsub_subscriptions.channels_and_patterns.items(): + entry = request.pubsub_subscriptions.channels_or_patterns_by_type[ + int(channel_type) + ] + for channel_pattern in channels_patterns: + entry.channels_or_patterns.append(str.encode(channel_pattern)) + return request + + def _is_pubsub_configured(self) -> bool: + return self.pubsub_subscriptions is not None + + def _get_pubsub_callback_and_context( + self, + ) -> Tuple[Optional[Callable[[CoreCommands.PubSubMsg, Any], None]], Any]: + if self.pubsub_subscriptions: + return self.pubsub_subscriptions.callback, self.pubsub_subscriptions.context + return None, None diff --git a/python/python/glide/constants.py b/python/python/glide/constants.py index 54232dd6c5..24c30d8de7 100644 --- a/python/python/glide/constants.py +++ b/python/python/glide/constants.py @@ -1,9 +1,9 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from typing import Dict, List, Literal, Optional, Set, TypeVar, Union +from typing import Dict, List, Literal, Mapping, Optional, Set, TypeVar, Union +from glide.protobuf.command_request_pb2 import CommandRequest from glide.protobuf.connection_request_pb2 import ConnectionRequest -from glide.protobuf.redis_request_pb2 import RedisRequest from glide.routes import ByAddressRoute, RandomNode, SlotIdRoute, SlotKeyRoute OK: str = "OK" @@ -18,16 +18,49 @@ int, None, Dict[str, T], + Mapping[str, "TResult"], float, Set[T], List[T], + bytes, + Dict[bytes, "TResult"], + Mapping[bytes, "TResult"], ] -TRequest = Union[RedisRequest, ConnectionRequest] +TRequest = Union[CommandRequest, ConnectionRequest] # When routing to a single node, response will be T # Otherwise, response will be : {Address : response , ... } with type of Dict[str, T]. -TClusterResponse = Union[T, Dict[str, T]] +TClusterResponse = Union[T, Dict[bytes, T]] TSingleNodeRoute = Union[RandomNode, SlotKeyRoute, SlotIdRoute, ByAddressRoute] # When specifying legacy path (path doesn't start with `$`), response will be T # Otherwise, (when specifying JSONPath), response will be List[Optional[T]]. # For more information, see: https://redis.io/docs/data-types/json/path/ . TJsonResponse = Union[T, List[Optional[T]]] +TEncodable = Union[str, bytes] +TFunctionListResponse = List[ + Mapping[ + bytes, + Union[bytes, List[Mapping[bytes, Union[bytes, Set[bytes]]]]], + ] +] +TFunctionStatsResponse = Mapping[ + bytes, + Union[ + None, + Mapping[ + bytes, Union[Mapping[bytes, Mapping[bytes, int]], bytes, int, List[bytes]] + ], + ], +] + +TXInfoStreamResponse = Mapping[ + bytes, Union[bytes, int, Mapping[bytes, Optional[List[List[bytes]]]]] +] +TXInfoStreamFullResponse = Mapping[ + bytes, + Union[ + bytes, + int, + Mapping[bytes, List[List[bytes]]], + List[Mapping[bytes, Union[bytes, int, List[List[Union[bytes, int]]]]]], + ], +] diff --git a/python/python/glide/exceptions.py b/python/python/glide/exceptions.py index 2b000e3e53..5c5659974c 100644 --- a/python/python/glide/exceptions.py +++ b/python/python/glide/exceptions.py @@ -1,9 +1,9 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from typing import Optional -class RedisError(Exception): +class GlideError(Exception): """ Base class for errors. """ @@ -15,7 +15,7 @@ def name(self): return self.__class__.__name__ -class ClosingError(RedisError): +class ClosingError(GlideError): """ Errors that report that the client has closed and is no longer usable. """ @@ -23,7 +23,7 @@ class ClosingError(RedisError): pass -class RequestError(RedisError): +class RequestError(GlideError): """ Errors that were reported during a request. """ @@ -54,3 +54,9 @@ class ConnectionError(RequestError): """ pass + + +class ConfigurationError(RequestError): + """ + Errors that are thrown when a request cannot be completed in current configuration settings. + """ diff --git a/python/python/glide/glide.pyi b/python/python/glide/glide.pyi index d155757bbd..ee053b1e44 100644 --- a/python/python/glide/glide.pyi +++ b/python/python/glide/glide.pyi @@ -1,10 +1,11 @@ from collections.abc import Callable from enum import Enum -from typing import Optional +from typing import List, Optional, Union from glide.constants import TResult DEFAULT_TIMEOUT_IN_MILLISECONDS: int = ... +MAX_REQUEST_ARGS_LEN: int = ... class Level(Enum): Error = 0 @@ -16,12 +17,18 @@ class Level(Enum): def is_lower(self, level: Level) -> bool: ... class Script: - def __init__(self, code: str) -> None: ... + def __init__(self, code: Union[str, bytes]) -> None: ... def get_hash(self) -> str: ... def __del__(self) -> None: ... +class ClusterScanCursor: + def __init__(self, cursor: Optional[str] = None) -> None: ... + def get_cursor(self) -> str: ... + def is_finished(self) -> bool: ... + def start_socket_listener_external(init_callback: Callable) -> None: ... def value_from_pointer(pointer: int) -> TResult: ... def create_leaked_value(message: str) -> int: ... +def create_leaked_bytes_vec(args_vec: List[bytes]) -> int: ... def py_init(level: Optional[Level], file_name: Optional[str]) -> Level: ... def py_log(log_level: Level, log_identifier: str, message: str) -> None: ... diff --git a/python/python/glide/glide_client.py b/python/python/glide/glide_client.py new file mode 100644 index 0000000000..1e6d2a5b63 --- /dev/null +++ b/python/python/glide/glide_client.py @@ -0,0 +1,577 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +import asyncio +import sys +import threading +from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast + +import async_timeout +from glide.async_commands.cluster_commands import ClusterCommands +from glide.async_commands.command_args import ObjectType +from glide.async_commands.core import CoreCommands +from glide.async_commands.standalone_commands import StandaloneCommands +from glide.config import BaseClientConfiguration +from glide.constants import DEFAULT_READ_BYTES_SIZE, OK, TEncodable, TRequest, TResult +from glide.exceptions import ( + ClosingError, + ConfigurationError, + ConnectionError, + ExecAbortError, + RequestError, + TimeoutError, +) +from glide.logger import Level as LogLevel +from glide.logger import Logger as ClientLogger +from glide.protobuf.command_request_pb2 import Command, CommandRequest, RequestType +from glide.protobuf.connection_request_pb2 import ConnectionRequest +from glide.protobuf.response_pb2 import RequestErrorType, Response +from glide.protobuf_codec import PartialMessageException, ProtobufCodec +from glide.routes import Route, set_protobuf_route +from typing_extensions import Self + +from .glide import ( + DEFAULT_TIMEOUT_IN_MILLISECONDS, + MAX_REQUEST_ARGS_LEN, + ClusterScanCursor, + create_leaked_bytes_vec, + start_socket_listener_external, + value_from_pointer, +) + + +def get_request_error_class( + error_type: Optional[RequestErrorType.ValueType], +) -> Type[RequestError]: + if error_type == RequestErrorType.Disconnect: + return ConnectionError + if error_type == RequestErrorType.ExecAbort: + return ExecAbortError + if error_type == RequestErrorType.Timeout: + return TimeoutError + if error_type == RequestErrorType.Unspecified: + return RequestError + return RequestError + + +class BaseClient(CoreCommands): + def __init__(self, config: BaseClientConfiguration): + """ + To create a new client, use the `create` classmethod + """ + self.config: BaseClientConfiguration = config + self._available_futures: Dict[int, asyncio.Future] = {} + self._available_callback_indexes: List[int] = list() + self._buffered_requests: List[TRequest] = list() + self._writer_lock = threading.Lock() + self.socket_path: Optional[str] = None + self._reader_task: Optional[asyncio.Task] = None + self._is_closed: bool = False + self._pubsub_futures: List[asyncio.Future] = [] + self._pubsub_lock = threading.Lock() + self._pending_push_notifications: List[Response] = list() + + @classmethod + async def create(cls, config: BaseClientConfiguration) -> Self: + """Creates a Glide client. + + Args: + config (ClientConfiguration): The client configurations. + If no configuration is provided, a default client to "localhost":6379 will be created. + + Returns: + Self: a Glide Client instance. + """ + config = config + self = cls(config) + init_future: asyncio.Future = asyncio.Future() + loop = asyncio.get_event_loop() + + def init_callback(socket_path: Optional[str], err: Optional[str]): + if err is not None: + raise ClosingError(err) + elif socket_path is None: + raise ClosingError( + "Socket initialization error: Missing valid socket path." + ) + else: + # Received socket path + self.socket_path = socket_path + loop.call_soon_threadsafe(init_future.set_result, True) + + start_socket_listener_external(init_callback=init_callback) + + # will log if the logger was created (wrapper or costumer) on info + # level or higher + ClientLogger.log(LogLevel.INFO, "connection info", "new connection established") + # Wait for the socket listener to complete its initialization + await init_future + # Create UDS connection + await self._create_uds_connection() + # Start the reader loop as a background task + self._reader_task = asyncio.create_task(self._reader_loop()) + # Set the client configurations + await self._set_connection_configurations() + return self + + async def _create_uds_connection(self) -> None: + try: + # Open an UDS connection + async with async_timeout.timeout(DEFAULT_TIMEOUT_IN_MILLISECONDS): + reader, writer = await asyncio.open_unix_connection( + path=self.socket_path + ) + self._reader = reader + self._writer = writer + except Exception as e: + await self.close(f"Failed to create UDS connection: {e}") + raise + + def __del__(self) -> None: + try: + if self._reader_task: + self._reader_task.cancel() + except RuntimeError as e: + if "no running event loop" in str(e): + # event loop already closed + pass + + async def close(self, err_message: Optional[str] = None) -> None: + """ + Terminate the client by closing all associated resources, including the socket and any active futures. + All open futures will be closed with an exception. + + Args: + err_message (Optional[str]): If not None, this error message will be passed along with the exceptions when closing all open futures. + Defaults to None. + """ + self._is_closed = True + for response_future in self._available_futures.values(): + if not response_future.done(): + err_message = "" if err_message is None else err_message + response_future.set_exception(ClosingError(err_message)) + try: + self._pubsub_lock.acquire() + for pubsub_future in self._pubsub_futures: + if not pubsub_future.done() and not pubsub_future.cancelled(): + pubsub_future.set_exception(ClosingError("")) + finally: + self._pubsub_lock.release() + + self._writer.close() + await self._writer.wait_closed() + self.__del__() + + def _get_future(self, callback_idx: int) -> asyncio.Future: + response_future: asyncio.Future = asyncio.Future() + self._available_futures.update({callback_idx: response_future}) + return response_future + + def _get_protobuf_conn_request(self) -> ConnectionRequest: + return self.config._create_a_protobuf_conn_request() + + async def _set_connection_configurations(self) -> None: + conn_request = self._get_protobuf_conn_request() + response_future: asyncio.Future = self._get_future(0) + await self._write_or_buffer_request(conn_request) + await response_future + if response_future.result() is not OK: + raise ClosingError(response_future.result()) + + def _create_write_task(self, request: TRequest): + asyncio.create_task(self._write_or_buffer_request(request)) + + async def _write_or_buffer_request(self, request: TRequest): + self._buffered_requests.append(request) + if self._writer_lock.acquire(False): + try: + while len(self._buffered_requests) > 0: + await self._write_buffered_requests_to_socket() + + finally: + self._writer_lock.release() + + async def _write_buffered_requests_to_socket(self) -> None: + requests = self._buffered_requests + self._buffered_requests = list() + b_arr = bytearray() + for request in requests: + ProtobufCodec.encode_delimited(b_arr, request) + self._writer.write(b_arr) + await self._writer.drain() + + def _encode_arg(self, arg: TEncodable) -> bytes: + """ + Converts a string argument to bytes. + + Args: + arg (str): An encodable argument. + + Returns: + bytes: The encoded argument as bytes. + """ + if isinstance(arg, str): + # TODO: Allow passing different encoding options + return bytes(arg, encoding="utf8") + return arg + + def _encode_and_sum_size( + self, + args_list: Optional[List[TEncodable]], + ) -> Tuple[List[bytes], int]: + """ + Encodes the list and calculates the total memory size. + + Args: + args_list (Optional[List[TEncodable]]): A list of strings to be converted to bytes. + If None or empty, returns ([], 0). + + Returns: + int: The total memory size of the encoded arguments in bytes. + """ + args_size = 0 + encoded_args_list: List[bytes] = [] + if not args_list: + return (encoded_args_list, args_size) + for arg in args_list: + encoded_arg = self._encode_arg(arg) if isinstance(arg, str) else arg + encoded_args_list.append(encoded_arg) + args_size += sys.getsizeof(encoded_arg) + return (encoded_args_list, args_size) + + async def _execute_command( + self, + request_type: RequestType.ValueType, + args: List[TEncodable], + route: Optional[Route] = None, + ) -> TResult: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + request = CommandRequest() + request.callback_idx = self._get_callback_index() + request.single_command.request_type = request_type + request.single_command.args_array.args[:] = [ + bytes(elem, encoding="utf8") if isinstance(elem, str) else elem + for elem in args + ] + (encoded_args, args_size) = self._encode_and_sum_size(args) + if args_size < MAX_REQUEST_ARGS_LEN: + request.single_command.args_array.args[:] = encoded_args + else: + request.single_command.args_vec_pointer = create_leaked_bytes_vec( + encoded_args + ) + set_protobuf_route(request, route) + return await self._write_request_await_response(request) + + async def _execute_transaction( + self, + commands: List[Tuple[RequestType.ValueType, List[TEncodable]]], + route: Optional[Route] = None, + ) -> List[TResult]: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + request = CommandRequest() + request.callback_idx = self._get_callback_index() + transaction_commands = [] + for requst_type, args in commands: + command = Command() + command.request_type = requst_type + # For now, we allow the user to pass the command as array of strings + # we convert them here into bytes (the datatype that our rust core expects) + (encoded_args, args_size) = self._encode_and_sum_size(args) + if args_size < MAX_REQUEST_ARGS_LEN: + command.args_array.args[:] = encoded_args + else: + command.args_vec_pointer = create_leaked_bytes_vec(encoded_args) + transaction_commands.append(command) + request.transaction.commands.extend(transaction_commands) + set_protobuf_route(request, route) + return await self._write_request_await_response(request) + + async def _execute_script( + self, + hash: str, + keys: Optional[List[Union[str, bytes]]] = None, + args: Optional[List[Union[str, bytes]]] = None, + route: Optional[Route] = None, + ) -> TResult: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + request = CommandRequest() + request.callback_idx = self._get_callback_index() + (encoded_keys, keys_size) = self._encode_and_sum_size(keys) + (encoded_args, args_size) = self._encode_and_sum_size(args) + if (keys_size + args_size) < MAX_REQUEST_ARGS_LEN: + request.script_invocation.hash = hash + request.script_invocation.keys[:] = encoded_keys + request.script_invocation.args[:] = encoded_args + + else: + request.script_invocation_pointers.hash = hash + request.script_invocation_pointers.keys_pointer = create_leaked_bytes_vec( + encoded_keys + ) + request.script_invocation_pointers.args_pointer = create_leaked_bytes_vec( + encoded_args + ) + set_protobuf_route(request, route) + return await self._write_request_await_response(request) + + async def get_pubsub_message(self) -> CoreCommands.PubSubMsg: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + + if not self.config._is_pubsub_configured(): + raise ConfigurationError( + "The operation will never complete since there was no pubsub subscriptions applied to the client." + ) + + if self.config._get_pubsub_callback_and_context()[0] is not None: + raise ConfigurationError( + "The operation will never complete since messages will be passed to the configured callback." + ) + + # locking might not be required + response_future: asyncio.Future = asyncio.Future() + try: + self._pubsub_lock.acquire() + self._pubsub_futures.append(response_future) + self._complete_pubsub_futures_safe() + finally: + self._pubsub_lock.release() + return await response_future + + def try_get_pubsub_message(self) -> Optional[CoreCommands.PubSubMsg]: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + + if not self.config._is_pubsub_configured(): + raise ConfigurationError( + "The operation will never succeed since there was no pubsbub subscriptions applied to the client." + ) + + if self.config._get_pubsub_callback_and_context()[0] is not None: + raise ConfigurationError( + "The operation will never succeed since messages will be passed to the configured callback." + ) + + # locking might not be required + msg: Optional[CoreCommands.PubSubMsg] = None + try: + self._pubsub_lock.acquire() + self._complete_pubsub_futures_safe() + while len(self._pending_push_notifications) and not msg: + push_notification = self._pending_push_notifications.pop(0) + msg = self._notification_to_pubsub_message_safe(push_notification) + finally: + self._pubsub_lock.release() + return msg + + def _cancel_pubsub_futures_with_exception_safe(self, exception: ConnectionError): + while len(self._pubsub_futures): + next_future = self._pubsub_futures.pop(0) + if not next_future.cancelled(): + next_future.set_exception(exception) + + def _notification_to_pubsub_message_safe( + self, response: Response + ) -> Optional[CoreCommands.PubSubMsg]: + pubsub_message = None + push_notification = cast( + Dict[str, Any], value_from_pointer(response.resp_pointer) + ) + message_kind = push_notification["kind"] + if message_kind == "Disconnection": + ClientLogger.log( + LogLevel.WARN, + "disconnect notification", + "Transport disconnected, messages might be lost", + ) + elif ( + message_kind == "Message" + or message_kind == "PMessage" + or message_kind == "SMessage" + ): + values: List = push_notification["values"] + if message_kind == "PMessage": + pubsub_message = BaseClient.PubSubMsg( + message=values[2], channel=values[1], pattern=values[0] + ) + else: + pubsub_message = BaseClient.PubSubMsg( + message=values[1], channel=values[0], pattern=None + ) + elif ( + message_kind == "PSubscribe" + or message_kind == "Subscribe" + or message_kind == "SSubscribe" + or message_kind == "Unsubscribe" + or message_kind == "PUnsubscribe" + or message_kind == "SUnsubscribe" + ): + pass + else: + ClientLogger.log( + LogLevel.WARN, + "unknown notification", + f"Unknown notification message: '{message_kind}'", + ) + + return pubsub_message + + def _complete_pubsub_futures_safe(self): + while len(self._pending_push_notifications) and len(self._pubsub_futures): + next_push_notification = self._pending_push_notifications.pop(0) + pubsub_message = self._notification_to_pubsub_message_safe( + next_push_notification + ) + if pubsub_message: + self._pubsub_futures.pop(0).set_result(pubsub_message) + + async def _write_request_await_response(self, request: CommandRequest): + # Create a response future for this request and add it to the available + # futures map + response_future = self._get_future(request.callback_idx) + self._create_write_task(request) + await response_future + return response_future.result() + + def _get_callback_index(self) -> int: + try: + return self._available_callback_indexes.pop() + except IndexError: + # The list is empty + return len(self._available_futures) + + async def _process_response(self, response: Response) -> None: + res_future = self._available_futures.pop(response.callback_idx, None) + if not res_future or response.HasField("closing_error"): + err_msg = ( + response.closing_error + if response.HasField("closing_error") + else f"Client Error - closing due to unknown error. callback index: {response.callback_idx}" + ) + if res_future is not None: + res_future.set_exception(ClosingError(err_msg)) + await self.close(err_msg) + raise ClosingError(err_msg) + else: + self._available_callback_indexes.append(response.callback_idx) + if response.HasField("request_error"): + error_type = get_request_error_class(response.request_error.type) + res_future.set_exception(error_type(response.request_error.message)) + elif response.HasField("resp_pointer"): + res_future.set_result(value_from_pointer(response.resp_pointer)) + elif response.HasField("constant_response"): + res_future.set_result(OK) + else: + res_future.set_result(None) + + async def _process_push(self, response: Response) -> None: + if response.HasField("closing_error") or not response.HasField("resp_pointer"): + err_msg = ( + response.closing_error + if response.HasField("closing_error") + else "Client Error - push notification without resp_pointer" + ) + await self.close(err_msg) + raise ClosingError(err_msg) + + try: + self._pubsub_lock.acquire() + callback, context = self.config._get_pubsub_callback_and_context() + if callback: + pubsub_message = self._notification_to_pubsub_message_safe(response) + if pubsub_message: + callback(pubsub_message, context) + else: + self._pending_push_notifications.append(response) + self._complete_pubsub_futures_safe() + finally: + self._pubsub_lock.release() + + async def _reader_loop(self) -> None: + # Socket reader loop + remaining_read_bytes = bytearray() + while True: + read_bytes = await self._reader.read(DEFAULT_READ_BYTES_SIZE) + if len(read_bytes) == 0: + err_msg = "The communication layer was unexpectedly closed." + await self.close(err_msg) + raise ClosingError(err_msg) + read_bytes = remaining_read_bytes + bytearray(read_bytes) + read_bytes_view = memoryview(read_bytes) + offset = 0 + while offset <= len(read_bytes): + try: + response, offset = ProtobufCodec.decode_delimited( + read_bytes, read_bytes_view, offset, Response + ) + except PartialMessageException: + # Recieved only partial response, break the inner loop + remaining_read_bytes = read_bytes[offset:] + break + response = cast(Response, response) + if response.is_push: + await self._process_push(response=response) + else: + await self._process_response(response=response) + + +class GlideClusterClient(BaseClient, ClusterCommands): + """ + Client used for connection to cluster servers. + For full documentation, see + https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#cluster + """ + + async def _cluster_scan( + self, + cursor: ClusterScanCursor, + match: Optional[TEncodable] = None, + count: Optional[int] = None, + type: Optional[ObjectType] = None, + ) -> List[Union[ClusterScanCursor, List[bytes]]]: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + request = CommandRequest() + request.callback_idx = self._get_callback_index() + # Take out the id string from the wrapping object + cursor_string = cursor.get_cursor() + request.cluster_scan.cursor = cursor_string + if match is not None: + request.cluster_scan.match_pattern = ( + self._encode_arg(match) if isinstance(match, str) else match + ) + if count is not None: + request.cluster_scan.count = count + if type is not None: + request.cluster_scan.object_type = type.value + response = await self._write_request_await_response(request) + return [ClusterScanCursor(bytes(response[0]).decode()), response[1]] + + def _get_protobuf_conn_request(self) -> ConnectionRequest: + return self.config._create_a_protobuf_conn_request(cluster_mode=True) + + +class GlideClient(BaseClient, StandaloneCommands): + """ + Client used for connection to standalone servers. + For full documentation, see + https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#redis-standalone + """ + + +TGlideClient = Union[GlideClient, GlideClusterClient] diff --git a/python/python/glide/logger.py b/python/python/glide/logger.py index ae8c09e9bc..2426136aad 100644 --- a/python/python/glide/logger.py +++ b/python/python/glide/logger.py @@ -1,4 +1,4 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from __future__ import annotations diff --git a/python/python/glide/protobuf_codec.py b/python/python/glide/protobuf_codec.py index 959637db58..859b85610e 100644 --- a/python/python/glide/protobuf_codec.py +++ b/python/python/glide/protobuf_codec.py @@ -1,4 +1,4 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import struct from typing import List, Tuple, Type diff --git a/python/python/glide/redis_client.py b/python/python/glide/redis_client.py deleted file mode 100644 index 1ae7b9a228..0000000000 --- a/python/python/glide/redis_client.py +++ /dev/null @@ -1,328 +0,0 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - -import asyncio -import threading -from typing import List, Optional, Tuple, Type, Union, cast - -import async_timeout -from glide.async_commands.cluster_commands import ClusterCommands -from glide.async_commands.core import CoreCommands -from glide.async_commands.standalone_commands import StandaloneCommands -from glide.config import BaseClientConfiguration -from glide.constants import DEFAULT_READ_BYTES_SIZE, OK, TRequest, TResult -from glide.exceptions import ( - ClosingError, - ConnectionError, - ExecAbortError, - RequestError, - TimeoutError, -) -from glide.logger import Level as LogLevel -from glide.logger import Logger as ClientLogger -from glide.protobuf.connection_request_pb2 import ConnectionRequest -from glide.protobuf.redis_request_pb2 import Command, RedisRequest, RequestType -from glide.protobuf.response_pb2 import RequestErrorType, Response -from glide.protobuf_codec import PartialMessageException, ProtobufCodec -from glide.routes import Route, set_protobuf_route -from typing_extensions import Self - -from .glide import ( - DEFAULT_TIMEOUT_IN_MILLISECONDS, - start_socket_listener_external, - value_from_pointer, -) - - -def get_request_error_class( - error_type: Optional[RequestErrorType.ValueType], -) -> Type[RequestError]: - if error_type == RequestErrorType.Disconnect: - return ConnectionError - if error_type == RequestErrorType.ExecAbort: - return ExecAbortError - if error_type == RequestErrorType.Timeout: - return TimeoutError - if error_type == RequestErrorType.Unspecified: - return RequestError - return RequestError - - -class BaseRedisClient(CoreCommands): - def __init__(self, config: BaseClientConfiguration): - """ - To create a new client, use the `create` classmethod - """ - self.config: BaseClientConfiguration = config - self._available_futures: dict[int, asyncio.Future] = {} - self._available_callback_indexes: List[int] = list() - self._buffered_requests: List[TRequest] = list() - self._writer_lock = threading.Lock() - self.socket_path: Optional[str] = None - self._reader_task: Optional[asyncio.Task] = None - self._is_closed: bool = False - - @classmethod - async def create(cls, config: BaseClientConfiguration) -> Self: - """Creates a Redis client. - - Args: - config (ClientConfiguration): The client configurations. - If no configuration is provided, a default client to "localhost":6379 will be created. - - Returns: - Self: a Redis Client instance. - """ - config = config - self = cls(config) - init_future: asyncio.Future = asyncio.Future() - loop = asyncio.get_event_loop() - - def init_callback(socket_path: Optional[str], err: Optional[str]): - if err is not None: - raise ClosingError(err) - elif socket_path is None: - raise ClosingError( - "Socket initialization error: Missing valid socket path." - ) - else: - # Received socket path - self.socket_path = socket_path - loop.call_soon_threadsafe(init_future.set_result, True) - - start_socket_listener_external(init_callback=init_callback) - - # will log if the logger was created (wrapper or costumer) on info - # level or higher - ClientLogger.log(LogLevel.INFO, "connection info", "new connection established") - # Wait for the socket listener to complete its initialization - await init_future - # Create UDS connection - await self._create_uds_connection() - # Start the reader loop as a background task - self._reader_task = asyncio.create_task(self._reader_loop()) - # Set the client configurations - await self._set_connection_configurations() - return self - - async def _create_uds_connection(self) -> None: - try: - # Open an UDS connection - async with async_timeout.timeout(DEFAULT_TIMEOUT_IN_MILLISECONDS): - reader, writer = await asyncio.open_unix_connection( - path=self.socket_path - ) - self._reader = reader - self._writer = writer - except Exception as e: - await self.close(f"Failed to create UDS connection: {e}") - raise - - def __del__(self) -> None: - try: - if self._reader_task: - self._reader_task.cancel() - except RuntimeError as e: - if "no running event loop" in str(e): - # event loop already closed - pass - - async def close(self, err_message: Optional[str] = None) -> None: - """ - Terminate the client by closing all associated resources, including the socket and any active futures. - All open futures will be closed with an exception. - - Args: - err_message (Optional[str]): If not None, this error message will be passed along with the exceptions when closing all open futures. - Defaults to None. - """ - self._is_closed = True - for response_future in self._available_futures.values(): - if not response_future.done(): - err_message = "" if err_message is None else err_message - response_future.set_exception(ClosingError(err_message)) - self._writer.close() - await self._writer.wait_closed() - self.__del__() - - def _get_future(self, callback_idx: int) -> asyncio.Future: - response_future: asyncio.Future = asyncio.Future() - self._available_futures.update({callback_idx: response_future}) - return response_future - - def _get_protobuf_conn_request(self) -> ConnectionRequest: - return self.config._create_a_protobuf_conn_request() - - async def _set_connection_configurations(self) -> None: - conn_request = self._get_protobuf_conn_request() - response_future: asyncio.Future = self._get_future(0) - await self._write_or_buffer_request(conn_request) - await response_future - if response_future.result() is not OK: - raise ClosingError(response_future.result()) - - def _create_write_task(self, request: TRequest): - asyncio.create_task(self._write_or_buffer_request(request)) - - async def _write_or_buffer_request(self, request: TRequest): - self._buffered_requests.append(request) - if self._writer_lock.acquire(False): - try: - while len(self._buffered_requests) > 0: - await self._write_buffered_requests_to_socket() - - finally: - self._writer_lock.release() - - async def _write_buffered_requests_to_socket(self) -> None: - requests = self._buffered_requests - self._buffered_requests = list() - b_arr = bytearray() - for request in requests: - ProtobufCodec.encode_delimited(b_arr, request) - self._writer.write(b_arr) - await self._writer.drain() - - async def _execute_command( - self, - request_type: RequestType.ValueType, - args: List[str], - route: Optional[Route] = None, - ) -> TResult: - if self._is_closed: - raise ClosingError( - "Unable to execute requests; the client is closed. Please create a new client." - ) - request = RedisRequest() - request.callback_idx = self._get_callback_index() - request.single_command.request_type = request_type - request.single_command.args_array.args[:] = args # TODO - use arg pointer - set_protobuf_route(request, route) - return await self._write_request_await_response(request) - - async def _execute_transaction( - self, - commands: List[Tuple[RequestType.ValueType, List[str]]], - route: Optional[Route] = None, - ) -> List[TResult]: - if self._is_closed: - raise ClosingError( - "Unable to execute requests; the client is closed. Please create a new client." - ) - request = RedisRequest() - request.callback_idx = self._get_callback_index() - transaction_commands = [] - for requst_type, args in commands: - command = Command() - command.request_type = requst_type - command.args_array.args[:] = args - transaction_commands.append(command) - request.transaction.commands.extend(transaction_commands) - set_protobuf_route(request, route) - return await self._write_request_await_response(request) - - async def _execute_script( - self, - hash: str, - keys: Optional[List[str]] = None, - args: Optional[List[str]] = None, - route: Optional[Route] = None, - ) -> TResult: - if self._is_closed: - raise ClosingError( - "Unable to execute requests; the client is closed. Please create a new client." - ) - request = RedisRequest() - request.callback_idx = self._get_callback_index() - request.script_invocation.hash = hash - request.script_invocation.args[:] = args if args is not None else [] - request.script_invocation.keys[:] = keys if keys is not None else [] - set_protobuf_route(request, route) - return await self._write_request_await_response(request) - - async def _write_request_await_response(self, request: RedisRequest): - # Create a response future for this request and add it to the available - # futures map - response_future = self._get_future(request.callback_idx) - self._create_write_task(request) - await response_future - return response_future.result() - - def _get_callback_index(self) -> int: - try: - return self._available_callback_indexes.pop() - except IndexError: - # The list is empty - return len(self._available_futures) - - async def _reader_loop(self) -> None: - # Socket reader loop - remaining_read_bytes = bytearray() - while True: - read_bytes = await self._reader.read(DEFAULT_READ_BYTES_SIZE) - if len(read_bytes) == 0: - err_msg = "The communication layer was unexpectedly closed." - await self.close(err_msg) - raise ClosingError(err_msg) - read_bytes = remaining_read_bytes + bytearray(read_bytes) - read_bytes_view = memoryview(read_bytes) - offset = 0 - while offset <= len(read_bytes): - try: - response, offset = ProtobufCodec.decode_delimited( - read_bytes, read_bytes_view, offset, Response - ) - except PartialMessageException: - # Recieved only partial response, break the inner loop - remaining_read_bytes = read_bytes[offset:] - break - response = cast(Response, response) - res_future = self._available_futures.pop(response.callback_idx, None) - if not res_future or response.HasField("closing_error"): - err_msg = ( - response.closing_error - if response.HasField("closing_error") - else f"Client Error - closing due to unknown error. callback index: {response.callback_idx}" - ) - if res_future is not None: - res_future.set_exception(ClosingError(err_msg)) - await self.close(err_msg) - raise ClosingError(err_msg) - else: - self._available_callback_indexes.append(response.callback_idx) - if response.HasField("request_error"): - error_type = get_request_error_class( - response.request_error.type - ) - res_future.set_exception( - error_type(response.request_error.message) - ) - elif response.HasField("resp_pointer"): - res_future.set_result(value_from_pointer(response.resp_pointer)) - elif response.HasField("constant_response"): - res_future.set_result(OK) - else: - res_future.set_result(None) - - -class RedisClusterClient(BaseRedisClient, ClusterCommands): - """ - Client used for connection to cluster Redis servers. - For full documentation, see - https://github.com/aws/babushka/wiki/Python-wrapper#redis-cluster - """ - - def _get_protobuf_conn_request(self) -> ConnectionRequest: - return self.config._create_a_protobuf_conn_request(cluster_mode=True) - - -class RedisClient(BaseRedisClient, StandaloneCommands): - """ - Client used for connection to standalone Redis servers. - For full documentation, see - https://github.com/aws/babushka/wiki/Python-wrapper#redis-standalone - """ - - pass - - -TRedisClient = Union[RedisClient, RedisClusterClient] diff --git a/python/python/glide/routes.py b/python/python/glide/routes.py index 29cf8b364c..580f85beb0 100644 --- a/python/python/glide/routes.py +++ b/python/python/glide/routes.py @@ -1,11 +1,11 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from enum import Enum from typing import Optional from glide.exceptions import RequestError -from glide.protobuf.redis_request_pb2 import RedisRequest, SimpleRoutes -from glide.protobuf.redis_request_pb2 import SlotTypes as ProtoSlotTypes +from glide.protobuf.command_request_pb2 import CommandRequest, SimpleRoutes +from glide.protobuf.command_request_pb2 import SlotTypes as ProtoSlotTypes class SlotType(Enum): @@ -24,6 +24,12 @@ def __init__(self) -> None: class AllNodes(Route): + """ + Route request to all nodes. + Warning: + Don't use it with write commands, they could be routed to a replica (RO) node and fail. + """ + pass @@ -32,6 +38,12 @@ class AllPrimaries(Route): class RandomNode(Route): + """ + Route request to a random node. + Warning: + Don't use it with write commands, because they could be randomly routed to a replica (RO) node and fail. + """ + pass @@ -80,7 +92,7 @@ def to_protobuf_slot_type(slot_type: SlotType) -> ProtoSlotTypes.ValueType: ) -def set_protobuf_route(request: RedisRequest, route: Optional[Route]) -> None: +def set_protobuf_route(request: CommandRequest, route: Optional[Route]) -> None: if route is None: return elif isinstance(route, AllNodes): diff --git a/python/python/tests/__init__.py b/python/python/tests/__init__.py index 9d4ea1a992..fa59791e66 100644 --- a/python/python/tests/__init__.py +++ b/python/python/tests/__init__.py @@ -1 +1 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 diff --git a/python/python/tests/conftest.py b/python/python/tests/conftest.py index 359dab6ee2..4fab580098 100644 --- a/python/python/tests/conftest.py +++ b/python/python/tests/conftest.py @@ -1,19 +1,19 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import random from typing import AsyncGenerator, List, Optional, Union import pytest from glide.config import ( - ClusterClientConfiguration, + GlideClientConfiguration, + GlideClusterClientConfiguration, NodeAddress, ProtocolVersion, - RedisClientConfiguration, - RedisCredentials, + ServerCredentials, ) +from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient from glide.logger import Level as logLevel from glide.logger import Logger -from glide.redis_client import RedisClient, RedisClusterClient, TRedisClient from tests.utils.cluster import RedisCluster DEFAULT_HOST = "localhost" @@ -28,14 +28,14 @@ def pytest_addoption(parser): "--host", default=DEFAULT_HOST, action="store", - help="Redis host endpoint, defaults to `%(default)s`", + help="Server host endpoint, defaults to `%(default)s`", ) parser.addoption( "--port", default=DEFAULT_PORT, action="store", - help="Redis port, defaults to `%(default)s`", + help="Server port, defaults to `%(default)s`", ) parser.addoption( @@ -62,6 +62,83 @@ def pytest_addoption(parser): default=[], ) + parser.addoption( + "--cluster-endpoints", + action="store", + help="""Comma-separated list of cluster endpoints for standalone cluster in the format host1:port1,host2:port2,... + Note: The cluster will be flashed between tests. + Example: + pytest --asyncio-mode=auto --cluster-endpoints=127.0.0.1:6379 + pytest --asyncio-mode=auto --cluster-endpoints=127.0.0.1:6379,127.0.0.1:6380 + """, + default=None, + ) + + parser.addoption( + "--standalone-endpoints", + action="store", + help="""Comma-separated list of cluster endpoints for cluster mode cluster in the format host1:port1,host2:port2,... + Note: The cluster will be flashed between tests. + Example: + pytest --asyncio-mode=auto --standalone-endpoints=127.0.0.1:6379 + pytest --asyncio-mode=auto --standalone-endpoints=127.0.0.1:6379,127.0.0.1:6380 + """, + default=None, + ) + + +def parse_endpoints(endpoints_str: str) -> List[List[str]]: + """ + Parse the endpoints string into a list of lists containing host and port. + """ + try: + endpoints = [endpoint.split(":") for endpoint in endpoints_str.split(",")] + for endpoint in endpoints: + if len(endpoint) != 2: + raise ValueError( + "Each endpoint should be in the format 'host:port'.\nEndpoints should be separated by commas." + ) + host, port = endpoint + if not host or not port.isdigit(): + raise ValueError( + "Both host and port should be specified and port should be a valid integer." + ) + return endpoints + except ValueError as e: + raise ValueError("Invalid endpoints format: " + str(e)) + + +def create_clusters(tls, load_module, cluster_endpoints, standalone_endpoints): + """ + Create Redis clusters based on the provided options. + """ + if cluster_endpoints or standalone_endpoints: + # Endpoints were passed by the caller, not creating clusters internally + if cluster_endpoints: + cluster_endpoints = parse_endpoints(cluster_endpoints) + pytest.redis_cluster = RedisCluster(tls=tls, addresses=cluster_endpoints) + if standalone_endpoints: + standalone_endpoints = parse_endpoints(standalone_endpoints) + pytest.standalone_cluster = RedisCluster( + tls=tls, addresses=standalone_endpoints + ) + else: + # No endpoints were provided, create new clusters + pytest.redis_cluster = RedisCluster( + tls=tls, + cluster_mode=True, + load_module=load_module, + addresses=cluster_endpoints, + ) + pytest.standalone_cluster = RedisCluster( + tls=tls, + cluster_mode=False, + shard_count=1, + replica_count=1, + load_module=load_module, + addresses=standalone_endpoints, + ) + @pytest.fixture(autouse=True, scope="session") def call_before_all_pytests(request): @@ -71,8 +148,10 @@ def call_before_all_pytests(request): """ tls = request.config.getoption("--tls") load_module = request.config.getoption("--load-module") - pytest.redis_cluster = RedisCluster(tls, True, load_module=load_module) - pytest.standalone_cluster = RedisCluster(tls, False, 1, 1, load_module=load_module) + cluster_endpoints = request.config.getoption("--cluster-endpoints") + standalone_endpoints = request.config.getoption("--standalone-endpoints") + + create_clusters(tls, load_module, cluster_endpoints, standalone_endpoints) def pytest_sessionfinish(session, exitstatus): @@ -93,42 +172,84 @@ def pytest_sessionfinish(session, exitstatus): pass +def pytest_collection_modifyitems(config, items): + """ + Modify collected test items. + + This function checks if cluster or standalone endpoints are provided. If so, it checks if the test requires + cluster mode and skips it accordingly. + """ + for item in items: + if config.getoption("--cluster-endpoints") or config.getoption( + "--standalone-endpoints" + ): + if "cluster_mode" in item.fixturenames: + cluster_mode_value = item.callspec.params.get("cluster_mode", None) + if cluster_mode_value is True and not config.getoption( + "--cluster-endpoints" + ): + item.add_marker( + pytest.mark.skip( + reason="Test skipped because cluster_mode=True and cluster endpoints weren't provided" + ) + ) + elif cluster_mode_value is False and not config.getoption( + "--standalone-endpoints" + ): + item.add_marker( + pytest.mark.skip( + reason="Test skipped because cluster_mode=False and standalone endpoints weren't provided" + ) + ) + + @pytest.fixture() -async def redis_client( +async def glide_client( request, cluster_mode: bool, protocol: ProtocolVersion -) -> AsyncGenerator[TRedisClient, None]: +) -> AsyncGenerator[TGlideClient, None]: "Get async socket client for tests" client = await create_client(request, cluster_mode, protocol=protocol) yield client + await test_teardown(request, cluster_mode, protocol) await client.close() async def create_client( request, cluster_mode: bool, - credentials: Optional[RedisCredentials] = None, + credentials: Optional[ServerCredentials] = None, database_id: int = 0, addresses: Optional[List[NodeAddress]] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, -) -> Union[RedisClient, RedisClusterClient]: + timeout: Optional[int] = None, + cluster_mode_pubsub: Optional[ + GlideClusterClientConfiguration.PubSubSubscriptions + ] = None, + standalone_mode_pubsub: Optional[ + GlideClientConfiguration.PubSubSubscriptions + ] = None, +) -> Union[GlideClient, GlideClusterClient]: # Create async socket client use_tls = request.config.getoption("--tls") if cluster_mode: assert type(pytest.redis_cluster) is RedisCluster assert database_id == 0 - seed_nodes = random.sample(pytest.redis_cluster.nodes_addr, k=3) - cluster_config = ClusterClientConfiguration( + k = min(3, len(pytest.redis_cluster.nodes_addr)) + seed_nodes = random.sample(pytest.redis_cluster.nodes_addr, k=k) + cluster_config = GlideClusterClientConfiguration( addresses=seed_nodes if addresses is None else addresses, use_tls=use_tls, credentials=credentials, client_name=client_name, protocol=protocol, + request_timeout=timeout, + pubsub_subscriptions=cluster_mode_pubsub, ) - return await RedisClusterClient.create(cluster_config) + return await GlideClusterClient.create(cluster_config) else: assert type(pytest.standalone_cluster) is RedisCluster - config = RedisClientConfiguration( + config = GlideClientConfiguration( addresses=( pytest.standalone_cluster.nodes_addr if addresses is None else addresses ), @@ -137,5 +258,20 @@ async def create_client( database_id=database_id, client_name=client_name, protocol=protocol, + request_timeout=timeout, + pubsub_subscriptions=standalone_mode_pubsub, ) - return await RedisClient.create(config) + return await GlideClient.create(config) + + +async def test_teardown(request, cluster_mode: bool, protocol: ProtocolVersion): + """ + Perform teardown tasks such as flushing all data from the cluster. + + We create a new client here because some tests load lots of data to the cluster, + which might cause the client to time out during flushing. Therefore, we create + a client with a custom timeout to ensure the operation completes successfully. + """ + client = await create_client(request, cluster_mode, protocol=protocol, timeout=2000) + await client.custom_command(["FLUSHALL"]) + await client.close() diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 5b6d437c59..d46e490b70 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -1,39 +1,84 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from __future__ import annotations import asyncio -import random -import string +import copy +import math import time -from datetime import datetime, timedelta, timezone -from typing import Dict, List, TypeVar, Union, cast +from datetime import date, datetime, timedelta, timezone +from typing import Any, Dict, List, Mapping, Tuple, Union, cast import pytest -from glide import ClosingError, RequestError, Script, TimeoutError +from glide import ClosingError, RequestError, Script +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldIncrBy, + BitFieldOverflow, + BitFieldSet, + BitmapIndexType, + BitOffset, + BitOffsetMultiplier, + BitOverflowControl, + BitwiseOperation, + OffsetOptions, + SignedEncoding, + UnsignedEncoding, +) +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, + ExpiryGetEx, ExpirySet, ExpiryType, - GeospatialData, + ExpiryTypeGetEx, + FlushMode, + FunctionRestorePolicy, InfBound, InfoSection, InsertPosition, UpdateOptions, ) from glide.async_commands.sorted_set import ( + AggregationType, + GeoSearchByBox, + GeoSearchByRadius, + GeoSearchCount, + GeospatialData, + GeoUnit, InfBound, LexBoundary, - Limit, RangeByIndex, RangeByLex, RangeByScore, ScoreBoundary, + ScoreFilter, +) +from glide.async_commands.stream import ( + ExclusiveIdBound, + IdBound, + MaxId, + MinId, + StreamAddOptions, + StreamClaimOptions, + StreamGroupOptions, + StreamPendingOptions, + StreamReadGroupOptions, + StreamReadOptions, + TrimByMaxLen, + TrimByMinId, ) -from glide.config import ProtocolVersion, RedisCredentials -from glide.constants import OK, TResult -from glide.redis_client import RedisClient, RedisClusterClient, TRedisClient +from glide.async_commands.transaction import ClusterTransaction, Transaction +from glide.config import ( + GlideClientConfiguration, + GlideClusterClientConfiguration, + ProtocolVersion, + ServerCredentials, +) +from glide.constants import OK, TEncodable, TFunctionStatsResponse, TResult +from glide.exceptions import TimeoutError as GlideTimeoutError +from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient from glide.routes import ( AllNodes, AllPrimaries, @@ -44,116 +89,69 @@ SlotKeyRoute, SlotType, ) -from packaging import version from tests.conftest import create_client - -T = TypeVar("T") - - -def is_single_response(response: T, single_res: T) -> bool: - """ - Recursively checks if a given response matches the type structure of single_res. - - Args: - response (T): The response to check. - single_res (T): An object with the expected type structure as an example for the single node response. - - Returns: - bool: True if response matches the structure of single_res, False otherwise. - - Example: - >>> is_single_response(["value"], LIST_STR) - True - >>> is_single_response([["value"]], LIST_STR) - False - """ - if isinstance(single_res, list) and isinstance(response, list): - return is_single_response(response[0], single_res[0]) - elif isinstance(response, type(single_res)): - return True - return False - - -def get_first_result(res: str | List[str] | List[List[str]] | Dict[str, str]) -> str: - while isinstance(res, list): - res = ( - res[1] - if not isinstance(res[0], list) and res[0].startswith("127.0.0.1") - else res[0] - ) - - if isinstance(res, dict): - res = list(res.values())[0] - - return res - - -def parse_info_response(res: str | Dict[str, str]) -> Dict[str, str]: - res = get_first_result(res) - info_lines = [ - line for line in res.splitlines() if line and not line.startswith("#") - ] - info_dict = {} - for line in info_lines: - splitted_line = line.split(":") - key = splitted_line[0] - value = splitted_line[1] - info_dict[key] = value - return info_dict - - -def get_random_string(length): - result_str = "".join(random.choice(string.ascii_letters) for i in range(length)) - return result_str - - -async def check_if_server_version_lt(client: TRedisClient, min_version: str) -> bool: - # TODO: change it to pytest fixture after we'll implement a sync client - info_str = await client.info([InfoSection.SERVER]) - redis_version = parse_info_response(info_str).get("redis_version") - assert redis_version is not None - return version.parse(redis_version) < version.parse(min_version) +from tests.utils.utils import ( + check_function_list_response, + check_function_stats_response, + check_if_server_version_lt, + compare_maps, + convert_bytes_to_string_object, + convert_string_to_bytes_object, + create_lua_lib_with_long_running_function, + generate_lua_lib_code, + get_first_result, + get_random_string, + is_single_response, + parse_info_response, +) @pytest.mark.asyncio -class TestRedisClients: +class TestGlideClients: @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_register_client_name_and_version(self, redis_client: TRedisClient): + async def test_register_client_name_and_version(self, glide_client: TGlideClient): min_version = "7.2.0" - if await check_if_server_version_lt(redis_client, min_version): + if await check_if_server_version_lt(glide_client, min_version): # TODO: change it to pytest fixture after we'll implement a sync client - return pytest.mark.skip(reason=f"Redis version required >= {min_version}") - info = await redis_client.custom_command(["CLIENT", "INFO"]) - assert type(info) is str - assert "lib-name=GlidePy" in info - assert "lib-ver=unknown" in info + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + info = await glide_client.custom_command(["CLIENT", "INFO"]) + assert isinstance(info, bytes) + info_str = info.decode() + assert "lib-name=GlidePy" in info_str + assert "lib-ver=unknown" in info_str @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_send_and_receive_large_values(self, redis_client: TRedisClient): - length = 2**16 - key = get_random_string(length) - value = get_random_string(length) + async def test_send_and_receive_large_values(self, request, cluster_mode, protocol): + glide_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=5000 + ) + length = 2**25 # 33mb + key = "0" * length + value = "0" * length assert len(key) == length assert len(value) == length - await redis_client.set(key, value) - assert await redis_client.get(key) == value + await glide_client.set(key, value) + assert await glide_client.get(key) == value.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_send_and_receive_non_ascii_unicode(self, redis_client: TRedisClient): + async def test_send_and_receive_non_ascii_unicode(self, glide_client: TGlideClient): key = "foo" value = "שלום hello 汉字" assert value == "שלום hello 汉字" - await redis_client.set(key, value) - assert await redis_client.get(key) == value + await glide_client.set(key, value) + assert await glide_client.get(key) == value.encode() + # check set and get in bytes + await glide_client.set(key.encode(), value.encode()) + assert await glide_client.get(key.encode()) == value.encode() @pytest.mark.parametrize("value_size", [100, 2**16]) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_client_handle_concurrent_workload_without_dropping_or_changing_values( - self, redis_client: TRedisClient, value_size + self, glide_client: TGlideClient, value_size ): num_of_concurrent_tasks = 100 running_tasks = set() @@ -162,8 +160,8 @@ async def exec_command(i): range_end = 1 if value_size > 100 else 100 for _ in range(range_end): value = get_random_string(value_size) - assert await redis_client.set(str(i), value) == OK - assert await redis_client.get(str(i)) == value + assert await glide_client.set(str(i), value) == OK + assert await glide_client.get(str(i)) == value.encode() for i in range(num_of_concurrent_tasks): task = asyncio.create_task(exec_command(i)) @@ -174,13 +172,13 @@ async def exec_command(i): @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_can_connect_with_auth_requirepass( - self, redis_client: TRedisClient, request + self, glide_client: TGlideClient, request ): - is_cluster = isinstance(redis_client, RedisClusterClient) + is_cluster = isinstance(glide_client, GlideClusterClient) password = "TEST_AUTH" - credentials = RedisCredentials(password) + credentials = ServerCredentials(password) try: - await redis_client.custom_command( + await glide_client.custom_command( ["CONFIG", "SET", "requirepass", password] ) @@ -189,39 +187,39 @@ async def test_can_connect_with_auth_requirepass( await create_client( request, is_cluster, - addresses=redis_client.config.addresses, + addresses=glide_client.config.addresses, ) auth_client = await create_client( request, is_cluster, credentials, - addresses=redis_client.config.addresses, + addresses=glide_client.config.addresses, ) key = get_random_string(10) assert await auth_client.set(key, key) == OK - assert await auth_client.get(key) == key + assert await auth_client.get(key) == key.encode() finally: # Reset the password auth_client = await create_client( request, is_cluster, credentials, - addresses=redis_client.config.addresses, + addresses=glide_client.config.addresses, ) await auth_client.custom_command(["CONFIG", "SET", "requirepass", ""]) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_can_connect_with_auth_acl( - self, redis_client: Union[RedisClient, RedisClusterClient], request + self, glide_client: Union[GlideClient, GlideClusterClient], request ): - is_cluster = isinstance(redis_client, RedisClusterClient) + is_cluster = isinstance(glide_client, GlideClusterClient) username = "testuser" password = "TEST_AUTH" try: assert ( - await redis_client.custom_command( + await glide_client.custom_command( [ "ACL", "SETUSER", @@ -239,47 +237,50 @@ async def test_can_connect_with_auth_acl( == OK ) key = get_random_string(10) - assert await redis_client.set(key, key) == OK - credentials = RedisCredentials(password, username) + assert await glide_client.set(key, key) == OK + credentials = ServerCredentials(password, username) testuser_client = await create_client( request, is_cluster, credentials, - addresses=redis_client.config.addresses, + addresses=glide_client.config.addresses, ) - assert await testuser_client.get(key) == key + assert await testuser_client.get(key) == key.encode() with pytest.raises(RequestError) as e: # This client isn't authorized to perform SET await testuser_client.set("foo", "bar") assert "NOPERM" in str(e) finally: # Delete this user - await redis_client.custom_command(["ACL", "DELUSER", username]) + await glide_client.custom_command(["ACL", "DELUSER", username]) - async def test_select_standalone_database_id(self, request): - redis_client = await create_client(request, cluster_mode=False, database_id=4) - client_info = await redis_client.custom_command(["CLIENT", "INFO"]) - assert "db=4" in client_info + @pytest.mark.parametrize("cluster_mode", [False]) + async def test_select_standalone_database_id(self, request, cluster_mode): + glide_client = await create_client( + request, cluster_mode=cluster_mode, database_id=4 + ) + client_info = await glide_client.custom_command(["CLIENT", "INFO"]) + assert b"db=4" in client_info @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_client_name(self, request, cluster_mode, protocol): - redis_client = await create_client( + glide_client = await create_client( request, cluster_mode=cluster_mode, client_name="TEST_CLIENT_NAME", protocol=protocol, ) - client_info = await redis_client.custom_command(["CLIENT", "INFO"]) - assert "name=TEST_CLIENT_NAME" in client_info + client_info = await glide_client.custom_command(["CLIENT", "INFO"]) + assert b"name=TEST_CLIENT_NAME" in client_info @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_closed_client_raises_error(self, redis_client: TRedisClient): - await redis_client.close() + async def test_closed_client_raises_error(self, glide_client: TGlideClient): + await glide_client.close() with pytest.raises(ClosingError) as e: - await redis_client.set("foo", "bar") + await glide_client.set("foo", "bar") assert "the client is closed" in str(e) @@ -288,1671 +289,9232 @@ class TestCommands: @pytest.mark.smoke_test @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_socket_set_get(self, redis_client: TRedisClient): + async def test_socket_set_get(self, glide_client: TGlideClient): key = get_random_string(10) value = datetime.now(timezone.utc).strftime("%m/%d/%Y, %H:%M:%S") - assert await redis_client.set(key, value) == OK - assert await redis_client.get(key) == value + assert await glide_client.set(key, value) == OK + assert await glide_client.get(key) == value.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP3]) - async def test_use_resp3_protocol(self, redis_client: TRedisClient): - result = cast(Dict[str, str], await redis_client.custom_command(["HELLO"])) + async def test_use_resp3_protocol(self, glide_client: TGlideClient): + result = cast(Dict[bytes, bytes], await glide_client.custom_command(["HELLO"])) - assert int(result["proto"]) == 3 + assert int(result[b"proto"]) == 3 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2]) - async def test_allow_opt_in_to_resp2_protocol(self, redis_client: TRedisClient): - result = cast(Dict[str, str], await redis_client.custom_command(["HELLO"])) + async def test_allow_opt_in_to_resp2_protocol(self, glide_client: TGlideClient): + result = cast(Dict[bytes, bytes], await glide_client.custom_command(["HELLO"])) - assert int(result["proto"]) == 2 + assert int(result[b"proto"]) == 2 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_conditional_set(self, redis_client: TRedisClient): + async def test_conditional_set(self, glide_client: TGlideClient): key = get_random_string(10) value = get_random_string(10) - res = await redis_client.set( + res = await glide_client.set( key, value, conditional_set=ConditionalChange.ONLY_IF_EXISTS ) assert res is None - res = await redis_client.set( + res = await glide_client.set( key, value, conditional_set=ConditionalChange.ONLY_IF_DOES_NOT_EXIST ) assert res == OK - assert await redis_client.get(key) == value - res = await redis_client.set( + assert await glide_client.get(key) == value.encode() + res = await glide_client.set( key, "foobar", conditional_set=ConditionalChange.ONLY_IF_DOES_NOT_EXIST ) assert res is None - assert await redis_client.get(key) == value + assert await glide_client.get(key) == value.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_set_return_old_value(self, redis_client: TRedisClient): + async def test_set_return_old_value(self, glide_client: TGlideClient): min_version = "6.2.0" - if await check_if_server_version_lt(redis_client, min_version): + if await check_if_server_version_lt(glide_client, min_version): # TODO: change it to pytest fixture after we'll implement a sync client - return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") key = get_random_string(10) value = get_random_string(10) - res = await redis_client.set(key, value) + res = await glide_client.set(key, value) assert res == OK - assert await redis_client.get(key) == value + assert await glide_client.get(key) == value.encode() new_value = get_random_string(10) - res = await redis_client.set(key, new_value, return_old_value=True) - assert res == value - assert await redis_client.get(key) == new_value + res = await glide_client.set(key, new_value, return_old_value=True) + assert res == value.encode() + assert await glide_client.get(key) == new_value.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_custom_command_single_arg(self, redis_client: TRedisClient): + async def test_custom_command_single_arg(self, glide_client: TGlideClient): # Test single arg command - res = await redis_client.custom_command(["PING"]) - assert res == "PONG" + res = await glide_client.custom_command(["PING"]) + assert res == b"PONG" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_custom_command_multi_arg(self, redis_client: TRedisClient): + async def test_custom_command_multi_arg(self, glide_client: TGlideClient): # Test multi args command - client_list = await redis_client.custom_command( + client_list = await glide_client.custom_command( ["CLIENT", "LIST", "TYPE", "NORMAL"] ) - assert isinstance(client_list, (str, list)) - res: str = get_first_result(client_list) + assert isinstance(client_list, (bytes, list)) + res = get_first_result(client_list) + assert res is not None + assert b"id" in res + assert b"cmd=client" in res + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_custom_command_multi_arg_in_TEncodable( + self, glide_client: TGlideClient + ): + # Test multi args command + client_list = await glide_client.custom_command( + ["CLIENT", b"LIST", "TYPE", b"NORMAL"] + ) + assert isinstance(client_list, (bytes, list)) + res = get_first_result(client_list) assert res is not None - assert "id" in res - assert "cmd=client" in res + assert b"id" in res + assert b"cmd=client" in res @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_custom_command_lower_and_upper_case( - self, redis_client: TRedisClient + self, glide_client: TGlideClient ): # Test multi args command - client_list = await redis_client.custom_command( + client_list = await glide_client.custom_command( ["CLIENT", "LIST", "TYPE", "NORMAL"] ) - assert isinstance(client_list, (str, list)) - res: str = get_first_result(client_list) + assert isinstance(client_list, (bytes, list)) + res = get_first_result(client_list) assert res is not None - assert "id" in res - assert "cmd=client" in res + assert b"id" in res + assert b"cmd=client" in res @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_request_error_raises_exception(self, redis_client: TRedisClient): + async def test_request_error_raises_exception(self, glide_client: TGlideClient): key = get_random_string(10) value = get_random_string(10) - await redis_client.set(key, value) + await glide_client.set(key, value) with pytest.raises(RequestError) as e: - await redis_client.custom_command(["HSET", key, "1", "bar"]) + await glide_client.custom_command(["HSET", key, "1", "bar"]) assert "WRONGTYPE" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_info_server_replication(self, redis_client: TRedisClient): - info = get_first_result(await redis_client.info([InfoSection.SERVER])) + async def test_info_server_replication(self, glide_client: TGlideClient): + info_res = get_first_result(await glide_client.info([InfoSection.SERVER])) + info = info_res.decode() assert "# Server" in info - cluster_mode = parse_info_response(info)["redis_mode"] - expected_cluster_mode = isinstance(redis_client, RedisClusterClient) + cluster_mode = parse_info_response(info_res)["redis_mode"] + expected_cluster_mode = isinstance(glide_client, GlideClusterClient) assert cluster_mode == "cluster" if expected_cluster_mode else "standalone" - info = get_first_result(await redis_client.info([InfoSection.REPLICATION])) + info = get_first_result( + await glide_client.info([InfoSection.REPLICATION]) + ).decode() assert "# Replication" in info assert "# Errorstats" not in info @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_info_default(self, redis_client: TRedisClient): - cluster_mode = isinstance(redis_client, RedisClusterClient) - info_result = await redis_client.info() + async def test_info_default(self, glide_client: TGlideClient): + cluster_mode = isinstance(glide_client, GlideClusterClient) + info_result = await glide_client.info() if cluster_mode: - cluster_nodes = await redis_client.custom_command(["CLUSTER", "NODES"]) - assert isinstance(cluster_nodes, (str, list)) + cluster_nodes = await glide_client.custom_command(["CLUSTER", "NODES"]) + assert isinstance(cluster_nodes, (bytes, list)) cluster_nodes = get_first_result(cluster_nodes) - expected_num_of_results = cluster_nodes.count("master") + expected_num_of_results = cluster_nodes.count(b"master") assert len(info_result) == expected_num_of_results info_result = get_first_result(info_result) - assert "# Memory" in info_result + assert b"# Memory" in info_result + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_select(self, glide_client: GlideClient): + assert await glide_client.select(0) == OK + key = get_random_string(10) + value = get_random_string(10) + assert await glide_client.set(key, value) == OK + assert await glide_client.get(key) == value.encode() + assert await glide_client.select(1) == OK + assert await glide_client.get(key) is None + assert await glide_client.select(0) == OK + assert await glide_client.get(key) == value.encode() + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_move(self, glide_client: GlideClient): + key = get_random_string(10) + value = get_random_string(10) + + assert await glide_client.select(0) == OK + assert await glide_client.move(key, 1) is False + + assert await glide_client.set(key, value) == OK + assert await glide_client.get(key) == value.encode() + + assert await glide_client.move(key, 1) is True + assert await glide_client.get(key) is None + assert await glide_client.select(1) == OK + assert await glide_client.get(key) == value.encode() + + with pytest.raises(RequestError) as e: + await glide_client.move(key, -1) @pytest.mark.parametrize("cluster_mode", [False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_select(self, redis_client: RedisClient): - assert await redis_client.select(0) == OK + async def test_move_with_bytes(self, glide_client: GlideClient): key = get_random_string(10) value = get_random_string(10) - assert await redis_client.set(key, value) == OK - assert await redis_client.get(key) == value - assert await redis_client.select(1) == OK - assert await redis_client.get(key) is None - assert await redis_client.select(0) == OK - assert await redis_client.get(key) == value + + assert await glide_client.select(0) == OK + + assert await glide_client.set(key, value) == OK + assert await glide_client.get(key.encode()) == value.encode() + + assert await glide_client.move(key.encode(), 1) is True + assert await glide_client.get(key) is None + assert await glide_client.get(key.encode()) is None + assert await glide_client.select(1) == OK + assert await glide_client.get(key) == value.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_delete(self, redis_client: TRedisClient): + async def test_delete(self, glide_client: TGlideClient): keys = [get_random_string(10), get_random_string(10), get_random_string(10)] value = get_random_string(10) - [await redis_client.set(key, value) for key in keys] - assert await redis_client.get(keys[0]) == value - assert await redis_client.get(keys[1]) == value - assert await redis_client.get(keys[2]) == value + value_encoded = value.encode() + [await glide_client.set(key, value) for key in keys] + assert await glide_client.get(keys[0]) == value_encoded + assert await glide_client.get(keys[1]) == value_encoded + assert await glide_client.get(keys[2]) == value_encoded delete_keys = keys + [get_random_string(10)] - assert await redis_client.delete(delete_keys) == 3 - assert await redis_client.delete(keys) == 0 + assert await glide_client.delete(delete_keys) == 3 + assert await glide_client.delete(keys) == 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_getdel(self, glide_client: TGlideClient): + key = get_random_string(10) + value = get_random_string(10) + non_existing_key = get_random_string(10) + list_key = get_random_string(10) + assert await glide_client.set(key, value) == "OK" + + # Retrieve and delete existing key + assert await glide_client.getdel(key) == value.encode() + assert await glide_client.get(key) is None + + # Try to get and delete a non-existing key + assert await glide_client.getdel(non_existing_key) is None + + assert await glide_client.lpush(list_key, [value]) == 1 + with pytest.raises(RequestError) as e: + await glide_client.getdel(list_key) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_getrange(self, glide_client: TGlideClient): + key = get_random_string(16) + value = get_random_string(10) + value_encoded = value.encode() + non_string_key = get_random_string(10) + + assert await glide_client.set(key, value) == OK + assert await glide_client.getrange(key, 0, 3) == value_encoded[:4] + assert await glide_client.getrange(key, -3, -1) == value_encoded[-3:] + assert await glide_client.getrange(key.encode(), -3, -1) == value_encoded[-3:] + assert await glide_client.getrange(key, 0, -1) == value_encoded + + # out of range + assert await glide_client.getrange(key, 10, 100) == value_encoded[10:] + assert await glide_client.getrange(key, -200, -3) == value_encoded[-200:-2] + assert await glide_client.getrange(key, 100, 200) == b"" + + # incorrect range + assert await glide_client.getrange(key, -1, -3) == b"" + + # a redis bug, fixed in version 8: https://github.com/redis/redis/issues/13207 + if await check_if_server_version_lt(glide_client, "8.0.0"): + assert await glide_client.getrange(key, -200, -100) == value[0].encode() + else: + assert await glide_client.getrange(key, -200, -100) == b"" + + if await check_if_server_version_lt(glide_client, "8.0.0"): + assert await glide_client.getrange(non_string_key, 0, -1) == b"" + + # non-string key + assert await glide_client.lpush(non_string_key, ["_"]) == 1 + with pytest.raises(RequestError): + await glide_client.getrange(non_string_key, 0, -1) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_config_reset_stat(self, redis_client: TRedisClient): + async def test_config_reset_stat(self, glide_client: TGlideClient): # we execute set and info so the commandstats will show `cmdstat_set::calls` greater than 1 # after the configResetStat call we initiate an info command and the the commandstats won't contain `cmdstat_set`. - await redis_client.set("foo", "bar") - info_stats = str(await redis_client.info([InfoSection.COMMAND_STATS])) + await glide_client.set("foo", "bar") + info_stats = str(await glide_client.info([InfoSection.COMMAND_STATS])) assert "cmdstat_set" in info_stats - assert await redis_client.config_resetstat() == OK - info_stats = str(await redis_client.info([InfoSection.COMMAND_STATS])) + assert await glide_client.config_resetstat() == OK + info_stats = str(await glide_client.info([InfoSection.COMMAND_STATS])) # 1 stands for the second info command assert "cmdstat_set" not in info_stats @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_config_rewrite(self, redis_client: TRedisClient): + async def test_config_rewrite(self, glide_client: TGlideClient): info_server = parse_info_response( - get_first_result(await redis_client.info([InfoSection.SERVER])) + get_first_result(await glide_client.info([InfoSection.SERVER])) ) if len(info_server["config_file"]) > 0: - assert await redis_client.config_rewrite() == OK + assert await glide_client.config_rewrite() == OK else: # We expect Redis to return an error since the test cluster doesn't use redis.conf file with pytest.raises(RequestError) as e: - await redis_client.config_rewrite() + await glide_client.config_rewrite() assert "The server is running without a config file" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_client_id(self, redis_client: TRedisClient): - client_id = await redis_client.client_id() + async def test_client_id(self, glide_client: TGlideClient): + client_id = await glide_client.client_id() assert type(client_id) is int assert client_id > 0 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_incr_commands_existing_key(self, redis_client: TRedisClient): + async def test_incr_commands_existing_key(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "10") == OK - assert await redis_client.incr(key) == 11 - assert await redis_client.get(key) == "11" - assert await redis_client.incrby(key, 4) == 15 - assert await redis_client.get(key) == "15" - assert await redis_client.incrbyfloat(key, 5.5) == 20.5 - assert await redis_client.get(key) == "20.5" + assert await glide_client.set(key, "10") == OK + assert await glide_client.incr(key) == 11 + assert await glide_client.get(key) == b"11" + assert await glide_client.incrby(key, 4) == 15 + assert await glide_client.get(key) == b"15" + assert await glide_client.incrbyfloat(key, 5.5) == 20.5 + assert await glide_client.get(key) == b"20.5" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_incr_commands_non_existing_key(self, redis_client: TRedisClient): + async def test_incr_commands_non_existing_key(self, glide_client: TGlideClient): key = get_random_string(10) key2 = get_random_string(10) key3 = get_random_string(10) - assert await redis_client.get(key) is None - assert await redis_client.incr(key) == 1 - assert await redis_client.get(key) == "1" + assert await glide_client.get(key) is None + assert await glide_client.incr(key) == 1 + assert await glide_client.get(key) == b"1" - assert await redis_client.get(key2) is None - assert await redis_client.incrby(key2, 3) == 3 - assert await redis_client.get(key2) == "3" + assert await glide_client.get(key2) is None + assert await glide_client.incrby(key2, 3) == 3 + assert await glide_client.get(key2) == b"3" - assert await redis_client.get(key3) is None - assert await redis_client.incrbyfloat(key3, 0.5) == 0.5 - assert await redis_client.get(key3) == "0.5" + assert await glide_client.get(key3) is None + assert await glide_client.incrbyfloat(key3, 0.5) == 0.5 + assert await glide_client.get(key3) == b"0.5" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_incr_commands_with_str_value(self, redis_client: TRedisClient): + async def test_incr_commands_with_str_value(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK + assert await glide_client.set(key, "foo") == OK with pytest.raises(RequestError) as e: - await redis_client.incr(key) + await glide_client.incr(key) assert "value is not an integer" in str(e) with pytest.raises(RequestError) as e: - await redis_client.incrby(key, 3) + await glide_client.incrby(key, 3) assert "value is not an integer" in str(e) with pytest.raises(RequestError) as e: - await redis_client.incrbyfloat(key, 3.5) + await glide_client.incrbyfloat(key, 3.5) assert "value is not a valid float" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_client_getname(self, redis_client: TRedisClient): - assert await redis_client.client_getname() is None + async def test_client_getname(self, glide_client: TGlideClient): + assert await glide_client.client_getname() is None assert ( - await redis_client.custom_command(["CLIENT", "SETNAME", "GlideConnection"]) + await glide_client.custom_command(["CLIENT", "SETNAME", "GlideConnection"]) == OK ) - assert await redis_client.client_getname() == "GlideConnection" + assert await glide_client.client_getname() == b"GlideConnection" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_mset_mget(self, redis_client: TRedisClient): + async def test_mset_mget(self, glide_client: TGlideClient): keys = [get_random_string(10), get_random_string(10), get_random_string(10)] non_existing_key = get_random_string(10) key_value_pairs = {key: value for key, value in zip(keys, keys)} - assert await redis_client.mset(key_value_pairs) == OK + assert await glide_client.mset(key_value_pairs) == OK # Add the non-existing key keys.append(non_existing_key) - mget_res = await redis_client.mget(keys) + mget_res = await glide_client.mget(keys) keys[-1] = None - assert mget_res == keys + assert mget_res == [key.encode() if key is not None else key for key in keys] + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_touch(self, glide_client: TGlideClient): + keys = [get_random_string(10), get_random_string(10)] + key_value_pairs = {key: value for key, value in zip(keys, keys)} + + assert await glide_client.mset(key_value_pairs) == OK + assert await glide_client.touch(keys) == 2 + + # 2 existing keys, one non-existing + assert await glide_client.touch([*keys, get_random_string(3)]) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_msetnx(self, glide_client: TGlideClient): + key1 = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + key3 = f"{{key}}-3{get_random_string(5)}" + non_existing = get_random_string(5) + value = get_random_string(5) + value_encoded = value.encode() + key_value_map1: Mapping[TEncodable, TEncodable] = {key1: value, key2: value} + key_value_map2: Mapping[TEncodable, TEncodable] = { + key2: get_random_string(5), + key3: value, + } + + assert await glide_client.msetnx(key_value_map1) is True + mget_res = await glide_client.mget([key1, key2, non_existing]) + assert mget_res == [value_encoded, value_encoded, None] + + assert await glide_client.msetnx(key_value_map2) is False + assert await glide_client.get(key3) is None + assert await glide_client.get(key2) == value_encoded @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_ping(self, redis_client: TRedisClient): - assert await redis_client.ping() == "PONG" - assert await redis_client.ping("HELLO") == "HELLO" + async def test_ping(self, glide_client: TGlideClient): + assert await glide_client.ping() == b"PONG" + assert await glide_client.ping("HELLO") == b"HELLO" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_config_get_set(self, redis_client: TRedisClient): - previous_timeout = await redis_client.config_get(["timeout"]) - assert await redis_client.config_set({"timeout": "1000"}) == OK - assert await redis_client.config_get(["timeout"]) == {"timeout": "1000"} + async def test_config_get_set(self, glide_client: TGlideClient): + previous_timeout = await glide_client.config_get(["timeout"]) + assert await glide_client.config_set({"timeout": "1000"}) == OK + assert await glide_client.config_get(["timeout"]) == {b"timeout": b"1000"} # revert changes to previous timeout - assert isinstance(previous_timeout, dict) - assert isinstance(previous_timeout["timeout"], str) + previous_timeout_decoded = convert_bytes_to_string_object(previous_timeout) + assert isinstance(previous_timeout_decoded, dict) + assert isinstance(previous_timeout_decoded["timeout"], str) assert ( - await redis_client.config_set({"timeout": previous_timeout["timeout"]}) + await glide_client.config_set( + {"timeout": previous_timeout_decoded["timeout"]} + ) == OK ) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_decr_decrby_existing_key(self, redis_client: TRedisClient): + async def test_decr_decrby_existing_key(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "10") == OK - assert await redis_client.decr(key) == 9 - assert await redis_client.get(key) == "9" - assert await redis_client.decrby(key, 4) == 5 - assert await redis_client.get(key) == "5" + assert await glide_client.set(key, "10") == OK + assert await glide_client.decr(key) == 9 + assert await glide_client.get(key) == b"9" + assert await glide_client.decrby(key, 4) == 5 + assert await glide_client.get(key) == b"5" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_decr_decrby_non_existing_key(self, redis_client: TRedisClient): + async def test_decr_decrby_non_existing_key(self, glide_client: TGlideClient): key = get_random_string(10) key2 = get_random_string(10) - assert await redis_client.get(key) is None - assert await redis_client.decr(key) == -1 - assert await redis_client.get(key) == "-1" + assert await glide_client.get(key) is None + assert await glide_client.decr(key) == -1 + assert await glide_client.get(key) == b"-1" - assert await redis_client.get(key2) is None - assert await redis_client.decrby(key2, 3) == -3 - assert await redis_client.get(key2) == "-3" + assert await glide_client.get(key2) is None + assert await glide_client.decrby(key2, 3) == -3 + assert await glide_client.get(key2) == b"-3" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_decr_with_str_value(self, redis_client: TRedisClient): + async def test_decr_with_str_value(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK + assert await glide_client.set(key, "foo") == OK with pytest.raises(RequestError) as e: - await redis_client.decr(key) + await glide_client.decr(key) assert "value is not an integer" in str(e) with pytest.raises(RequestError) as e: - await redis_client.decrby(key, 3) + await glide_client.decrby(key, 3) assert "value is not an integer" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hset_hget_hgetall(self, redis_client: TRedisClient): + async def test_setrange(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + + # test new key and existing key + assert await glide_client.setrange(key1, 0, "Hello World") == 11 + assert await glide_client.setrange(key1, 6, "GLIDE") == 11 + + # offset > len + assert await glide_client.setrange(key1, 15, "GLIDE") == 20 + + # negative offset + with pytest.raises(RequestError): + assert await glide_client.setrange(key1, -1, "GLIDE") + + # non-string key throws RequestError + assert await glide_client.lpush(key2, ["_"]) == 1 + with pytest.raises(RequestError): + assert await glide_client.setrange(key2, 0, "_") + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_hset_hget_hgetall(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) field2 = get_random_string(5) field_value_map = {field: "value", field2: "value2"} - assert await redis_client.hset(key, field_value_map) == 2 - assert await redis_client.hget(key, field) == "value" - assert await redis_client.hget(key, field2) == "value2" - assert await redis_client.hget(key, "non_existing_field") is None + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hget(key, field) == b"value" + assert await glide_client.hget(key, field2) == b"value2" + assert await glide_client.hget(key, "non_existing_field") is None - assert await redis_client.hgetall(key) == {field: "value", field2: "value2"} - assert await redis_client.hgetall("non_existing_field") == {} + hgetall_map = await glide_client.hgetall(key) + expected_map = { + field.encode(): b"value", + field2.encode(): b"value2", + } + assert compare_maps(hgetall_map, expected_map) is True + assert await glide_client.hgetall("non_existing_field") == {} @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hdel(self, redis_client: TRedisClient): + async def test_hdel(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) field2 = get_random_string(5) field3 = get_random_string(5) field_value_map = {field: "value", field2: "value2", field3: "value3"} - assert await redis_client.hset(key, field_value_map) == 3 - assert await redis_client.hdel(key, [field, field2]) == 2 - assert await redis_client.hdel(key, ["nonExistingField"]) == 0 - assert await redis_client.hdel("nonExistingKey", [field3]) == 0 + assert await glide_client.hset(key, field_value_map) == 3 + assert await glide_client.hdel(key, [field, field2]) == 2 + assert await glide_client.hdel(key, ["nonExistingField"]) == 0 + assert await glide_client.hdel("nonExistingKey", [field3]) == 0 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hsetnx(self, redis_client: TRedisClient): + async def test_hsetnx(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) - assert await redis_client.hsetnx(key, field, "value") == True - assert await redis_client.hsetnx(key, field, "new value") == False - assert await redis_client.hget(key, field) == "value" + assert await glide_client.hsetnx(key, field, "value") == True + assert await glide_client.hsetnx(key, field, "new value") == False + assert await glide_client.hget(key, field) == b"value" key = get_random_string(5) - assert await redis_client.set(key, "value") == OK + assert await glide_client.set(key, "value") == OK with pytest.raises(RequestError): - await redis_client.hsetnx(key, field, "value") + await glide_client.hsetnx(key, field, "value") @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hmget(self, redis_client: TRedisClient): + async def test_hmget(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) field2 = get_random_string(5) field_value_map = {field: "value", field2: "value2"} - assert await redis_client.hset(key, field_value_map) == 2 - assert await redis_client.hmget(key, [field, "nonExistingField", field2]) == [ - "value", + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hmget(key, [field, "nonExistingField", field2]) == [ + b"value", None, - "value2", + b"value2", ] - assert await redis_client.hmget("nonExistingKey", [field, field2]) == [ + assert await glide_client.hmget("nonExistingKey", [field, field2]) == [ None, None, ] @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hset_without_data(self, redis_client: TRedisClient): + async def test_hset_without_data(self, glide_client: TGlideClient): with pytest.raises(RequestError) as e: - await redis_client.hset("key", {}) + await glide_client.hset("key", {}) assert "wrong number of arguments" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hincrby_hincrbyfloat(self, redis_client: TRedisClient): + async def test_hincrby_hincrbyfloat(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) field_value_map = {field: "10"} - assert await redis_client.hset(key, field_value_map) == 1 - assert await redis_client.hincrby(key, field, 1) == 11 - assert await redis_client.hincrby(key, field, 4) == 15 - assert await redis_client.hincrbyfloat(key, field, 1.5) == 16.5 + assert await glide_client.hset(key, field_value_map) == 1 + assert await glide_client.hincrby(key, field, 1) == 11 + assert await glide_client.hincrby(key, field, 4) == 15 + assert await glide_client.hincrbyfloat(key, field, 1.5) == 16.5 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hincrby_non_existing_key_field(self, redis_client: TRedisClient): + async def test_hincrby_non_existing_key_field(self, glide_client: TGlideClient): key = get_random_string(10) key2 = get_random_string(10) field = get_random_string(5) field_value_map = {field: "10"} - assert await redis_client.hincrby("nonExistingKey", field, 1) == 1 - assert await redis_client.hset(key, field_value_map) == 1 - assert await redis_client.hincrby(key, "nonExistingField", 2) == 2 - assert await redis_client.hset(key2, field_value_map) == 1 - assert await redis_client.hincrbyfloat(key2, "nonExistingField", -0.5) == -0.5 + assert await glide_client.hincrby("nonExistingKey", field, 1) == 1 + assert await glide_client.hset(key, field_value_map) == 1 + assert await glide_client.hincrby(key, "nonExistingField", 2) == 2 + assert await glide_client.hset(key2, field_value_map) == 1 + assert await glide_client.hincrbyfloat(key2, "nonExistingField", -0.5) == -0.5 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hincrby_invalid_value(self, redis_client: TRedisClient): + async def test_hincrby_invalid_value(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) field_value_map = {field: "value"} - assert await redis_client.hset(key, field_value_map) == 1 + assert await glide_client.hset(key, field_value_map) == 1 with pytest.raises(RequestError) as e: - await redis_client.hincrby(key, field, 2) + await glide_client.hincrby(key, field, 2) assert "hash value is not an integer" in str(e) with pytest.raises(RequestError) as e: - await redis_client.hincrbyfloat(key, field, 1.5) + await glide_client.hincrbyfloat(key, field, 1.5) assert "hash value is not a float" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hexist(self, redis_client: TRedisClient): + async def test_hexist(self, glide_client: TGlideClient): key = get_random_string(10) field = get_random_string(5) field2 = get_random_string(5) field_value_map = {field: "value", field2: "value2"} - assert await redis_client.hset(key, field_value_map) == 2 - assert await redis_client.hexists(key, field) - assert not await redis_client.hexists(key, "nonExistingField") - assert not await redis_client.hexists("nonExistingKey", field2) + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hexists(key, field) + assert not await glide_client.hexists(key, "nonExistingField") + assert not await glide_client.hexists("nonExistingKey", field2) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hlen(self, redis_client: TRedisClient): + async def test_hlen(self, glide_client: TGlideClient): key = get_random_string(10) key2 = get_random_string(5) field = get_random_string(5) field2 = get_random_string(5) field_value_map = {field: "value", field2: "value2"} - assert await redis_client.hset(key, field_value_map) == 2 - assert await redis_client.hlen(key) == 2 - assert await redis_client.hdel(key, [field]) == 1 - assert await redis_client.hlen(key) == 1 - assert await redis_client.hlen("non_existing_hash") == 0 + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hlen(key) == 2 + assert await glide_client.hdel(key, [field]) == 1 + assert await glide_client.hlen(key) == 1 + assert await glide_client.hlen("non_existing_hash") == 0 - assert await redis_client.set(key2, "value") == OK + assert await glide_client.set(key2, "value") == OK with pytest.raises(RequestError): - await redis_client.hlen(key2) + await glide_client.hlen(key2) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hvals(self, redis_client: TRedisClient): + async def test_hvals(self, glide_client: TGlideClient): key = get_random_string(10) key2 = get_random_string(5) field = get_random_string(5) field2 = get_random_string(5) field_value_map = {field: "value", field2: "value2"} - assert await redis_client.hset(key, field_value_map) == 2 - assert await redis_client.hvals(key) == ["value", "value2"] - assert await redis_client.hdel(key, [field]) == 1 - assert await redis_client.hvals(key) == ["value2"] - assert await redis_client.hvals("non_existing_key") == [] + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hvals(key) == [b"value", b"value2"] + assert await glide_client.hdel(key, [field]) == 1 + assert await glide_client.hvals(key) == [b"value2"] + assert await glide_client.hvals("non_existing_key") == [] - assert await redis_client.set(key2, "value") == OK + assert await glide_client.set(key2, "value") == OK with pytest.raises(RequestError): - await redis_client.hvals(key2) + await glide_client.hvals(key2) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_hkeys(self, redis_client: TRedisClient): + async def test_hkeys(self, glide_client: TGlideClient): key = get_random_string(10) key2 = get_random_string(5) field = get_random_string(5) field2 = get_random_string(5) field_value_map = {field: "value", field2: "value2"} - assert await redis_client.hset(key, field_value_map) == 2 - assert await redis_client.hkeys(key) == [field, field2] - assert await redis_client.hdel(key, [field]) == 1 - assert await redis_client.hkeys(key) == [field2] - assert await redis_client.hkeys("non_existing_key") == [] + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hkeys(key) == [ + field.encode(), + field2.encode(), + ] + assert await glide_client.hdel(key, [field]) == 1 + assert await glide_client.hkeys(key) == [field2.encode()] + assert await glide_client.hkeys("non_existing_key") == [] - assert await redis_client.set(key2, "value") == OK + assert await glide_client.set(key2, "value") == OK with pytest.raises(RequestError): - await redis_client.hkeys(key2) + await glide_client.hkeys(key2) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_lpush_lpop_lrange(self, redis_client: TRedisClient): + async def test_hrandfield(self, glide_client: TGlideClient): key = get_random_string(10) - value_list = ["value4", "value3", "value2", "value1"] + key2 = get_random_string(5) + field = get_random_string(5) + field2 = get_random_string(5) + field_value_map = {field: "value", field2: "value2"} + + assert await glide_client.hset(key, field_value_map) == 2 + assert await glide_client.hrandfield(key) in [ + field.encode(), + field2.encode(), + ] + assert await glide_client.hrandfield("non_existing_key") is None - assert await redis_client.lpush(key, value_list) == 4 - assert await redis_client.lpop(key) == value_list[-1] - assert await redis_client.lrange(key, 0, -1) == value_list[-2::-1] - assert await redis_client.lpop_count(key, 2) == value_list[-2:0:-1] - assert await redis_client.lrange("non_existing_key", 0, -1) == [] - assert await redis_client.lpop("non_existing_key") is None + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.hrandfield(key2) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_lpush_lpop_lrange_wrong_type_raise_error( - self, redis_client: TRedisClient - ): + async def test_hrandfield_count(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK + key2 = get_random_string(5) + field = get_random_string(5) + field2 = get_random_string(5) + field_value_map = {field: "value", field2: "value2"} - with pytest.raises(RequestError) as e: - await redis_client.lpush(key, ["bar"]) - assert "Operation against a key holding the wrong kind of value" in str(e) + assert await glide_client.hset(key, field_value_map) == 2 + # Unique values are expected as count is positive + rand_fields = await glide_client.hrandfield_count(key, 4) + assert len(rand_fields) == 2 + assert set(rand_fields) == {field.encode(), field2.encode()} - with pytest.raises(RequestError) as e: - await redis_client.lpop(key) - assert "Operation against a key holding the wrong kind of value" in str(e) + # Duplicate values are expected as count is negative + rand_fields = await glide_client.hrandfield_count(key, -4) + assert len(rand_fields) == 4 + for rand_field in rand_fields: + assert rand_field in [field.encode(), field2.encode()] - with pytest.raises(RequestError) as e: - await redis_client.lrange(key, 0, -1) - assert "Operation against a key holding the wrong kind of value" in str(e) + assert await glide_client.hrandfield_count(key, 0) == [] + assert await glide_client.hrandfield_count("non_existing_key", 4) == [] + + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.hrandfield_count(key2, 5) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_lpushx(self, redis_client: TRedisClient): - key1 = get_random_string(10) - key2 = get_random_string(10) + async def test_hrandfield_withvalues(self, glide_client: TGlideClient): + key = get_random_string(10) + key2 = get_random_string(5) + field = get_random_string(5) + field2 = get_random_string(5) + field_value_map = {field: "value", field2: "value2"} - # new key - assert await redis_client.lpushx(key1, ["1"]) == 0 - assert await redis_client.lrange(key1, 0, -1) == [] - # existing key - assert await redis_client.lpush(key1, ["0"]) == 1 - assert await redis_client.lpushx(key1, ["1", "2", "3"]) == 4 - assert await redis_client.lrange(key1, 0, -1) == ["3", "2", "1", "0"] - # key exists, but not a list - assert await redis_client.set(key2, "bar") == OK - with pytest.raises(RequestError) as e: - await redis_client.lpushx(key2, ["_"]) - # incorrect arguments + assert await glide_client.hset(key, field_value_map) == 2 + # Unique values are expected as count is positive + rand_fields_with_values = await glide_client.hrandfield_withvalues(key, 4) + assert len(rand_fields_with_values) == 2 + for field_with_value in rand_fields_with_values: + assert field_with_value in [ + [field.encode(), b"value"], + [field2.encode(), b"value2"], + ] + + # Duplicate values are expected as count is negative + rand_fields_with_values = await glide_client.hrandfield_withvalues(key, -4) + assert len(rand_fields_with_values) == 4 + for field_with_value in rand_fields_with_values: + assert field_with_value in [ + [field.encode(), b"value"], + [field2.encode(), b"value2"], + ] + + assert await glide_client.hrandfield_withvalues(key, 0) == [] + assert await glide_client.hrandfield_withvalues("non_existing_key", 4) == [] + + assert await glide_client.set(key2, "value") == OK with pytest.raises(RequestError): - await redis_client.lpushx(key1, []) + await glide_client.hrandfield_withvalues(key2, 5) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_lindex(self, redis_client: TRedisClient): + async def test_hstrlen(self, glide_client: TGlideClient): key = get_random_string(10) - value_list = [get_random_string(5), get_random_string(5)] - assert await redis_client.lpush(key, value_list) == 2 - assert await redis_client.lindex(key, 0) == value_list[1] - assert await redis_client.lindex(key, 1) == value_list[0] - assert await redis_client.lindex(key, 3) is None - assert await redis_client.lindex("non_existing_key", 0) is None + + assert await glide_client.hstrlen(key, "field") == 0 + assert await glide_client.hset(key, {"field": "value"}) == 1 + assert await glide_client.hstrlen(key, "field") == 5 + + assert await glide_client.hstrlen(key, "field2") == 0 + + await glide_client.set(key, "value") + with pytest.raises(RequestError): + await glide_client.hstrlen(key, "field") @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_rpush_rpop(self, redis_client: TRedisClient): + async def test_lpush_lpop_lrange(self, glide_client: TGlideClient): key = get_random_string(10) - value_list = ["value4", "value3", "value2", "value1"] - - assert await redis_client.rpush(key, value_list) == 4 - assert await redis_client.rpop(key) == value_list[-1] + value_list: List[TEncodable] = ["value4", "value3", "value2", "value1"] - assert await redis_client.rpop_count(key, 2) == value_list[-2:0:-1] - assert await redis_client.rpop("non_existing_key") is None + assert await glide_client.lpush(key, value_list) == 4 + assert await glide_client.lpop(key) == cast(str, value_list[-1]).encode() + assert await glide_client.lrange(key, 0, -1) == convert_string_to_bytes_object( + value_list[-2::-1] + ) + assert await glide_client.lpop_count(key, 2) == convert_string_to_bytes_object( + value_list[-2:0:-1] + ) + assert await glide_client.lrange("non_existing_key", 0, -1) == [] + assert await glide_client.lpop("non_existing_key") is None @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_rpush_rpop_wrong_type_raise_error(self, redis_client: TRedisClient): + async def test_lpush_lpop_lrange_wrong_type_raise_error( + self, glide_client: TGlideClient + ): key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK + assert await glide_client.set(key, "foo") == OK + + with pytest.raises(RequestError) as e: + await glide_client.lpush(key, ["bar"]) + assert "Operation against a key holding the wrong kind of value" in str(e) with pytest.raises(RequestError) as e: - await redis_client.rpush(key, ["bar"]) + await glide_client.lpop(key) assert "Operation against a key holding the wrong kind of value" in str(e) with pytest.raises(RequestError) as e: - await redis_client.rpop(key) + await glide_client.lrange(key, 0, -1) assert "Operation against a key holding the wrong kind of value" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_rpushx(self, redis_client: TRedisClient): + async def test_lpushx(self, glide_client: TGlideClient): key1 = get_random_string(10) key2 = get_random_string(10) # new key - assert await redis_client.rpushx(key1, ["1"]) == 0 - assert await redis_client.lrange(key1, 0, -1) == [] + assert await glide_client.lpushx(key1, ["1"]) == 0 + assert await glide_client.lrange(key1, 0, -1) == [] # existing key - assert await redis_client.rpush(key1, ["0"]) == 1 - assert await redis_client.rpushx(key1, ["1", "2", "3"]) == 4 - assert await redis_client.lrange(key1, 0, -1) == ["0", "1", "2", "3"] - # key existing, but it is not a list - assert await redis_client.set(key2, "bar") == OK + assert await glide_client.lpush(key1, ["0"]) == 1 + assert await glide_client.lpushx(key1, ["1", "2", "3"]) == 4 + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["3", "2", "1", "0"] + ) + # key exists, but not a list + assert await glide_client.set(key2, "bar") == OK with pytest.raises(RequestError) as e: - await redis_client.rpushx(key2, ["_"]) + await glide_client.lpushx(key2, ["_"]) # incorrect arguments with pytest.raises(RequestError): - await redis_client.rpushx(key2, []) + await glide_client.lpushx(key1, []) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_linsert(self, redis_client: TRedisClient): - key1 = get_random_string(10) - key2 = get_random_string(10) - - assert await redis_client.lpush(key1, ["4", "3", "2", "1"]) == 4 - assert await redis_client.linsert(key1, InsertPosition.BEFORE, "2", "1.5") == 5 - assert await redis_client.linsert(key1, InsertPosition.AFTER, "3", "3.5") == 6 - assert await redis_client.lrange(key1, 0, -1) == [ - "1", - "1.5", - "2", - "3", - "3.5", - "4", - ] + async def test_blpop(self, glide_client: TGlideClient): + key1 = f"{{test}}-1-f{get_random_string(10)}" + key2 = f"{{test}}-2-f{get_random_string(10)}" + value1 = "value1" + value2 = "value2" + value_list: List[TEncodable] = [value1, value2] + + assert await glide_client.lpush(key1, value_list) == 2 + assert await glide_client.blpop( + [key1, key2], 0.5 + ) == convert_string_to_bytes_object([key1, value2]) + # ensure that command doesn't time out even if timeout > request timeout (250ms by default) + assert await glide_client.blpop(["non_existent_key"], 0.5) is None - assert ( - await redis_client.linsert( - "non_existing_key", InsertPosition.BEFORE, "pivot", "elem" - ) - == 0 - ) - assert await redis_client.linsert(key1, InsertPosition.AFTER, "5", "6") == -1 - - # key exists, but it is not a list - assert await redis_client.set(key2, "value") == OK + # key exists, but not a list + assert await glide_client.set("foo", "bar") with pytest.raises(RequestError): - await redis_client.linsert(key2, InsertPosition.AFTER, "p", "e") + await glide_client.blpop(["foo"], 0.001) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_sadd_srem_smembers_scard(self, redis_client: TRedisClient): - key = get_random_string(10) - value_list = ["member1", "member2", "member3", "member4"] - - assert await redis_client.sadd(key, value_list) == 4 - assert await redis_client.srem(key, ["member4", "nonExistingMember"]) == 1 + async def endless_blpop_call(): + await glide_client.blpop(["non_existent_key"], 0) - assert set(await redis_client.smembers(key)) == set(value_list[:3]) - - assert await redis_client.srem(key, ["member1"]) == 1 - assert await redis_client.scard(key) == 2 + # blpop is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_blpop_call(), timeout=3) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_sadd_srem_smembers_scard_non_existing_key( - self, redis_client: TRedisClient - ): - non_existing_key = get_random_string(10) - assert await redis_client.srem(non_existing_key, ["member"]) == 0 - assert await redis_client.scard(non_existing_key) == 0 - assert await redis_client.smembers(non_existing_key) == set() + async def test_lmpop(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{test}}-1-f{get_random_string(10)}" + key2 = f"{{test}}-2-f{get_random_string(10)}" + key3 = f"{{test}}-3-f{get_random_string(10)}" + + # Initialize the lists + assert await glide_client.lpush(key1, ["3", "2", "1"]) == 3 + assert await glide_client.lpush(key2, ["6", "5", "4"]) == 3 + + # Pop from LEFT + result = await glide_client.lmpop([key1, key2], ListDirection.LEFT, 2) + expected_result = {key1: ["1", "2"]} + assert compare_maps(result, expected_result) is True + + # Pop from RIGHT + result = await glide_client.lmpop([key2, key1], ListDirection.RIGHT, 2) + expected_result = {key2: ["6", "5"]} + assert compare_maps(result, expected_result) is True + + # Pop without count (default is 1) + result = await glide_client.lmpop([key1, key2], ListDirection.LEFT) + expected_result = {key1: ["3"]} + assert compare_maps(result, expected_result) is True + + # Non-existing key + result = await glide_client.lmpop([key3], ListDirection.LEFT, 1) + assert result is None + + # Non-list key + assert await glide_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await glide_client.lmpop([key3], ListDirection.LEFT, 1) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_sadd_srem_smembers_scard_wrong_type_raise_error( - self, redis_client: TRedisClient - ): - key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK - - with pytest.raises(RequestError) as e: - await redis_client.sadd(key, ["bar"]) - assert "Operation against a key holding the wrong kind of value" in str(e) - - with pytest.raises(RequestError) as e: - await redis_client.srem(key, ["bar"]) - assert "Operation against a key holding the wrong kind of value" in str(e) + async def test_blmpop(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{test}}-1-f{get_random_string(10)}" + key2 = f"{{test}}-2-f{get_random_string(10)}" + key3 = f"{{test}}-3-f{get_random_string(10)}" + key4 = f"{{test}}-4-f{get_random_string(10)}" + + # Initialize the lists + assert await glide_client.lpush(key1, ["3", "2", "1"]) == 3 + assert await glide_client.lpush(key2, ["6", "5", "4"]) == 3 + + # Pop from LEFT with blocking + result = await glide_client.blmpop([key1, key2], ListDirection.LEFT, 0.1, 2) + expected_result = {key1: ["1", "2"]} + assert compare_maps(result, expected_result) is True + + # Pop from RIGHT with blocking + result = await glide_client.blmpop([key2, key1], ListDirection.RIGHT, 0.1, 2) + expected_result = {key2: ["6", "5"]} + assert compare_maps(result, expected_result) is True + + # Pop without count (default is 1) + result = await glide_client.blmpop([key1, key2], ListDirection.LEFT, 0.1) + expected_result = {key1: ["3"]} + assert compare_maps(result, expected_result) is True + + # Non-existing key with blocking + result = await glide_client.blmpop([key3], ListDirection.LEFT, 0.1, 1) + assert result is None + + # Non-list key with blocking + assert await glide_client.set(key4, "value") == OK + with pytest.raises(RequestError): + await glide_client.blmpop([key4], ListDirection.LEFT, 0.1, 1) - with pytest.raises(RequestError) as e: - await redis_client.scard(key) - assert "Operation against a key holding the wrong kind of value" in str(e) + # BLMPOP is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + async def endless_blmpop_call(): + await glide_client.blmpop([key3], ListDirection.LEFT, 0, 1) - with pytest.raises(RequestError) as e: - await redis_client.smembers(key) - assert "Operation against a key holding the wrong kind of value" in str(e) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_blmpop_call(), timeout=3) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_sismember(self, redis_client: TRedisClient): + async def test_lindex(self, glide_client: TGlideClient): key = get_random_string(10) - member = get_random_string(5) - assert await redis_client.sadd(key, [member]) == 1 - assert await redis_client.sismember(key, member) - assert not await redis_client.sismember(key, get_random_string(5)) - assert not await redis_client.sismember("non_existing_key", member) + value_list = [get_random_string(5), get_random_string(5)] + assert await glide_client.lpush(key, value_list) == 2 + assert await glide_client.lindex(key, 0) == value_list[1].encode() + assert await glide_client.lindex(key, 1) == value_list[0].encode() + assert await glide_client.lindex(key, 3) is None + assert await glide_client.lindex("non_existing_key", 0) is None @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_spop(self, redis_client: TRedisClient): + async def test_rpush_rpop(self, glide_client: TGlideClient): key = get_random_string(10) - member = get_random_string(5) - assert await redis_client.sadd(key, [member]) == 1 - assert await redis_client.spop(key) == member - - member2 = get_random_string(5) - member3 = get_random_string(5) - assert await redis_client.sadd(key, [member, member2, member3]) == 3 - assert await redis_client.spop_count(key, 4) == {member, member2, member3} + value_list: List[TEncodable] = ["value4", "value3", "value2", "value1"] - assert await redis_client.scard(key) == 0 + assert await glide_client.rpush(key, value_list) == 4 + assert await glide_client.rpop(key) == cast(str, value_list[-1]).encode() - assert await redis_client.spop("non_existing_key") == None - assert await redis_client.spop_count("non_existing_key", 3) == set() + assert await glide_client.rpop_count(key, 2) == convert_string_to_bytes_object( + value_list[-2:0:-1] + ) + assert await glide_client.rpop("non_existing_key") is None @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_ltrim(self, redis_client: TRedisClient): + async def test_rpush_rpop_wrong_type_raise_error(self, glide_client: TGlideClient): key = get_random_string(10) - value_list = ["value4", "value3", "value2", "value1"] + assert await glide_client.set(key, "foo") == OK - assert await redis_client.lpush(key, value_list) == 4 - assert await redis_client.ltrim(key, 0, 1) == OK - assert await redis_client.lrange(key, 0, -1) == ["value1", "value2"] + with pytest.raises(RequestError) as e: + await glide_client.rpush(key, ["bar"]) + assert "Operation against a key holding the wrong kind of value" in str(e) - assert await redis_client.ltrim(key, 4, 2) == OK - assert await redis_client.lrange(key, 0, -1) == [] + with pytest.raises(RequestError) as e: + await glide_client.rpop(key) + assert "Operation against a key holding the wrong kind of value" in str(e) - assert await redis_client.ltrim("non_existing_key", 0, 1) == OK + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_rpushx(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) - assert await redis_client.set(key, "foo") == OK + # new key + assert await glide_client.rpushx(key1, ["1"]) == 0 + assert await glide_client.lrange(key1, 0, -1) == [] + # existing key + assert await glide_client.rpush(key1, ["0"]) == 1 + assert await glide_client.rpushx(key1, ["1", "2", "3"]) == 4 + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["0", "1", "2", "3"] + ) + # key existing, but it is not a list + assert await glide_client.set(key2, "bar") == OK with pytest.raises(RequestError) as e: - await redis_client.ltrim(key, 0, 1) - assert "Operation against a key holding the wrong kind of value" in str(e) + await glide_client.rpushx(key2, ["_"]) + # incorrect arguments + with pytest.raises(RequestError): + await glide_client.rpushx(key2, []) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_lrem(self, redis_client: TRedisClient): - key = get_random_string(10) - value_list = ["value1", "value2", "value1", "value1", "value2"] + async def test_brpop(self, glide_client: TGlideClient): + key1 = f"{{test}}-1-f{get_random_string(10)}" + key2 = f"{{test}}-2-f{get_random_string(10)}" + value1 = "value1" + value2 = "value2" + value_list: List[TEncodable] = [value1, value2] - assert await redis_client.lpush(key, value_list) == 5 + assert await glide_client.lpush(key1, value_list) == 2 + # ensure that command doesn't time out even if timeout > request timeout (250ms by default) + assert await glide_client.brpop( + [key1, key2], 0.5 + ) == convert_string_to_bytes_object([key1, value1]) - assert await redis_client.lrem(key, 2, "value1") == 2 - assert await redis_client.lrange(key, 0, -1) == ["value2", "value2", "value1"] + assert await glide_client.brpop(["non_existent_key"], 0.5) is None - assert await redis_client.lrem(key, -1, "value2") == 1 - assert await redis_client.lrange(key, 0, -1) == ["value2", "value1"] + # key exists, but not a list + assert await glide_client.set("foo", "bar") + with pytest.raises(RequestError): + await glide_client.brpop(["foo"], 0.001) - assert await redis_client.lrem(key, 0, "value2") == 1 - assert await redis_client.lrange(key, 0, -1) == ["value1"] + async def endless_brpop_call(): + await glide_client.brpop(["non_existent_key"], 0) - assert await redis_client.lrem("non_existing_key", 2, "value") == 0 + # brpop is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_brpop_call(), timeout=3) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_llen(self, redis_client: TRedisClient): + async def test_linsert(self, glide_client: TGlideClient): key1 = get_random_string(10) key2 = get_random_string(10) - value_list = ["value4", "value3", "value2", "value1"] - assert await redis_client.lpush(key1, value_list) == 4 - assert await redis_client.llen(key1) == 4 + assert await glide_client.lpush(key1, ["4", "3", "2", "1"]) == 4 + assert await glide_client.linsert(key1, InsertPosition.BEFORE, "2", "1.5") == 5 + assert await glide_client.linsert(key1, InsertPosition.AFTER, "3", "3.5") == 6 + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + [ + "1", + "1.5", + "2", + "3", + "3.5", + "4", + ] + ) - assert await redis_client.llen("non_existing_key") == 0 + assert ( + await glide_client.linsert( + "non_existing_key", InsertPosition.BEFORE, "pivot", "elem" + ) + == 0 + ) + assert await glide_client.linsert(key1, InsertPosition.AFTER, "5", "6") == -1 - assert await redis_client.set(key2, "foo") == OK - with pytest.raises(RequestError) as e: - await redis_client.llen(key2) - assert "Operation against a key holding the wrong kind of value" in str(e) + # key exists, but it is not a list + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.linsert(key2, InsertPosition.AFTER, "p", "e") @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_strlen(self, redis_client: TRedisClient): - key1 = get_random_string(10) - key2 = get_random_string(10) - value_list = ["value4", "value3", "value2", "value1"] + async def test_lmove(self, glide_client: TGlideClient): + key1 = "{SameSlot}" + get_random_string(10) + key2 = "{SameSlot}" + get_random_string(10) - assert await redis_client.set(key1, "foo") == OK - assert await redis_client.strlen(key1) == 3 - assert await redis_client.strlen("non_existing_key") == 0 + # Initialize the lists + assert await glide_client.lpush(key1, ["2", "1"]) == 2 + assert await glide_client.lpush(key2, ["4", "3"]) == 2 - assert await redis_client.lpush(key2, value_list) == 4 - with pytest.raises(RequestError): - assert await redis_client.strlen(key2) + # Move from LEFT to LEFT + assert ( + await glide_client.lmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT) + == b"1" + ) + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["2"] + ) + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3", "4"] + ) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_rename(self, redis_client: TRedisClient): - key1 = "{" + get_random_string(10) + "}" - assert await redis_client.set(key1, "foo") == OK - assert await redis_client.rename(key1, key1 + "_rename") == OK - assert await redis_client.exists([key1 + "_rename"]) == 1 + # Move from LEFT to RIGHT + assert ( + await glide_client.lmove( + key1, key2, ListDirection.LEFT, ListDirection.RIGHT + ) + == b"2" + ) + assert await glide_client.lrange(key1, 0, -1) == [] + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3", "4", "2"] + ) - with pytest.raises(RequestError): - assert await redis_client.rename( - "{same_slot}" + "non_existing_key", "{same_slot}" + "_rename" + # Move from RIGHT to LEFT - non-existing destination key + assert ( + await glide_client.lmove( + key2, key1, ListDirection.RIGHT, ListDirection.LEFT ) + == b"2" + ) + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3", "4"] + ) + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["2"] + ) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_exists(self, redis_client: TRedisClient): - keys = [get_random_string(10), get_random_string(10)] + # Move from RIGHT to RIGHT + assert ( + await glide_client.lmove( + key2, key1, ListDirection.RIGHT, ListDirection.RIGHT + ) + == b"4" + ) + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3"] + ) + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["2", "4"] + ) + + # Non-existing source key + assert ( + await glide_client.lmove( + "{SameSlot}non_existing_key", + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ) + is None + ) - assert await redis_client.set(keys[0], "value") == OK - assert await redis_client.exists(keys) == 1 + # Non-list source key + key3 = get_random_string(10) + assert await glide_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await glide_client.lmove(key3, key1, ListDirection.LEFT, ListDirection.LEFT) - assert await redis_client.set(keys[1], "value") == OK - assert await redis_client.exists(keys) == 2 - keys.append("non_existing_key") - assert await redis_client.exists(keys) == 2 + # Non-list destination key + with pytest.raises(RequestError): + await glide_client.lmove(key1, key3, ListDirection.LEFT, ListDirection.LEFT) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_unlink(self, redis_client: TRedisClient): - key1 = get_random_string(10) - key2 = get_random_string(10) - key3 = get_random_string(10) + async def test_blmove(self, glide_client: TGlideClient): + key1 = "{SameSlot}" + get_random_string(10) + key2 = "{SameSlot}" + get_random_string(10) - assert await redis_client.set(key1, "value") == OK - assert await redis_client.set(key2, "value") == OK - assert await redis_client.set(key3, "value") == OK - assert await redis_client.unlink([key1, key2, "non_existing_key", key3]) == 3 + # Initialize the lists + assert await glide_client.lpush(key1, ["2", "1"]) == 2 + assert await glide_client.lpush(key2, ["4", "3"]) == 2 - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_expire_pexpire_ttl_with_positive_timeout( - self, redis_client: TRedisClient - ): - key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK + # Move from LEFT to LEFT with blocking + assert ( + await glide_client.blmove( + key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + == b"1" + ) + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["2"] + ) + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3", "4"] + ) - assert await redis_client.expire(key, 10) == 1 - assert await redis_client.ttl(key) in range(11) + # Move from LEFT to RIGHT with blocking + assert ( + await glide_client.blmove( + key1, key2, ListDirection.LEFT, ListDirection.RIGHT, 0.1 + ) + == b"2" + ) + assert await glide_client.lrange(key1, 0, -1) == [] + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3", "4", "2"] + ) - # set command clears the timeout. - assert await redis_client.set(key, "bar") == OK - if await check_if_server_version_lt(redis_client, "7.0.0"): - assert await redis_client.pexpire(key, 10000) - else: - assert await redis_client.pexpire(key, 10000, ExpireOptions.HasNoExpiry) - assert await redis_client.ttl(key) in range(11) + # Move from RIGHT to LEFT non-existing destination with blocking + assert ( + await glide_client.blmove( + key2, key1, ListDirection.RIGHT, ListDirection.LEFT, 0.1 + ) + == b"2" + ) + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3", "4"] + ) + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["2"] + ) - if await check_if_server_version_lt(redis_client, "7.0.0"): - assert await redis_client.expire(key, 15) - else: - assert await redis_client.expire(key, 15, ExpireOptions.HasExistingExpiry) - assert await redis_client.ttl(key) in range(16) + # Move from RIGHT to RIGHT with blocking + assert ( + await glide_client.blmove( + key2, key1, ListDirection.RIGHT, ListDirection.RIGHT, 0.1 + ) + == b"4" + ) + assert await glide_client.lrange(key2, 0, -1) == convert_string_to_bytes_object( + ["1", "3"] + ) + assert await glide_client.lrange(key1, 0, -1) == convert_string_to_bytes_object( + ["2", "4"] + ) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_expireat_pexpireat_ttl_with_positive_timeout( - self, redis_client: TRedisClient - ): - key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK - current_time = int(time.time()) + # Non-existing source key with blocking + assert ( + await glide_client.blmove( + "{SameSlot}non_existing_key", + key1, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ) + is None + ) - assert await redis_client.expireat(key, current_time + 10) == 1 - assert await redis_client.ttl(key) in range(11) - if await check_if_server_version_lt(redis_client, "7.0.0"): - assert await redis_client.expireat(key, current_time + 50) == 1 - else: - assert ( - await redis_client.expireat( - key, current_time + 50, ExpireOptions.NewExpiryGreaterThanCurrent - ) - == 1 + # Non-list source key with blocking + key3 = get_random_string(10) + assert await glide_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await glide_client.blmove( + key3, key1, ListDirection.LEFT, ListDirection.LEFT, 0.1 ) - assert await redis_client.ttl(key) in range(51) - # set command clears the timeout. - assert await redis_client.set(key, "bar") == OK - current_time_ms = int(time.time() * 1000) - if not await check_if_server_version_lt(redis_client, "7.0.0"): - assert not await redis_client.pexpireat( - key, current_time_ms + 50000, ExpireOptions.HasExistingExpiry + # Non-list destination key with blocking + with pytest.raises(RequestError): + await glide_client.blmove( + key1, key3, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + + # BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + async def endless_blmove_call(): + await glide_client.blmove( + "{SameSlot}non_existing_key", + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0, ) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_blmove_call(), timeout=3) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_expire_pexpire_expireat_pexpireat_past_or_negative_timeout( - self, redis_client: TRedisClient - ): + async def test_lset(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "foo") == OK - assert await redis_client.ttl(key) == -1 + element = get_random_string(5) + values = [get_random_string(5) for _ in range(4)] + + # key does not exist + with pytest.raises(RequestError): + await glide_client.lset("non_existing_key", 0, element) + + # pushing elements to list + await glide_client.lpush(key, values) == 4 + + # index out of range + with pytest.raises(RequestError): + await glide_client.lset(key, 10, element) - assert await redis_client.expire(key, -10) == 1 - assert await redis_client.ttl(key) == -2 + # assert lset result + assert await glide_client.lset(key, 0, element) == OK - assert await redis_client.set(key, "foo") == OK - assert await redis_client.pexpire(key, -10000) - assert await redis_client.ttl(key) == -2 + values = [element] + values[:-1][::-1] + assert await glide_client.lrange(key, 0, -1) == convert_string_to_bytes_object( + values + ) - assert await redis_client.set(key, "foo") == OK - assert await redis_client.expireat(key, int(time.time()) - 50) == 1 - assert await redis_client.ttl(key) == -2 + # assert lset with a negative index for the last element in the list + assert await glide_client.lset(key, -1, element) == OK - assert await redis_client.set(key, "foo") == OK - assert await redis_client.pexpireat(key, int(time.time() * 1000) - 50000) - assert await redis_client.ttl(key) == -2 + values[-1] = element + assert await glide_client.lrange(key, 0, -1) == convert_string_to_bytes_object( + values + ) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_expire_pexpire_expireAt_pexpireAt_ttl_non_existing_key( - self, redis_client: TRedisClient - ): + async def test_sadd_srem_smembers_scard(self, glide_client: TGlideClient): key = get_random_string(10) + value_list: List[TEncodable] = ["member1", "member2", "member3", "member4"] + + assert await glide_client.sadd(key, value_list) == 4 + assert await glide_client.srem(key, ["member4", "nonExistingMember"]) == 1 - assert await redis_client.expire(key, 10) == 0 - assert not await redis_client.pexpire(key, 10000) - assert await redis_client.expireat(key, int(time.time()) + 50) == 0 - assert not await redis_client.pexpireat(key, int(time.time() * 1000) + 50000) - assert await redis_client.ttl(key) == -2 + assert set(await glide_client.smembers(key)) == set( + cast(list, convert_string_to_bytes_object(value_list[:3])) + ) + + assert await glide_client.srem(key, ["member1"]) == 1 + assert await glide_client.scard(key) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sadd_srem_smembers_scard_non_existing_key( + self, glide_client: TGlideClient + ): + non_existing_key = get_random_string(10) + assert await glide_client.srem(non_existing_key, ["member"]) == 0 + assert await glide_client.scard(non_existing_key) == 0 + assert await glide_client.smembers(non_existing_key) == set() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_pttl(self, redis_client: TRedisClient): + async def test_sadd_srem_smembers_scard_wrong_type_raise_error( + self, glide_client: TGlideClient + ): key = get_random_string(10) - assert await redis_client.pttl(key) == -2 - current_time = int(time.time()) + assert await glide_client.set(key, "foo") == OK + + with pytest.raises(RequestError) as e: + await glide_client.sadd(key, ["bar"]) + assert "Operation against a key holding the wrong kind of value" in str(e) - assert await redis_client.set(key, "value") == OK - assert await redis_client.pttl(key) == -1 + with pytest.raises(RequestError) as e: + await glide_client.srem(key, ["bar"]) + assert "Operation against a key holding the wrong kind of value" in str(e) - assert await redis_client.expire(key, 10) - assert 0 < await redis_client.pttl(key) <= 10000 + with pytest.raises(RequestError) as e: + await glide_client.scard(key) + assert "Operation against a key holding the wrong kind of value" in str(e) - assert await redis_client.expireat(key, current_time + 20) - assert 0 < await redis_client.pttl(key) <= 20000 + with pytest.raises(RequestError) as e: + await glide_client.smembers(key) + assert "Operation against a key holding the wrong kind of value" in str(e) - assert await redis_client.pexpireat(key, current_time * 1000 + 30000) - assert 0 < await redis_client.pttl(key) <= 30000 + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sismember(self, glide_client: TGlideClient): + key = get_random_string(10) + member = get_random_string(5) + assert await glide_client.sadd(key, [member]) == 1 + assert await glide_client.sismember(key, member) + assert not await glide_client.sismember(key, get_random_string(5)) + assert not await glide_client.sismember("non_existing_key", member) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_persist(self, redis_client: TRedisClient): + async def test_spop(self, glide_client: TGlideClient): key = get_random_string(10) - assert await redis_client.set(key, "value") == OK - assert not await redis_client.persist(key) + member = get_random_string(5) + assert await glide_client.sadd(key, [member]) == 1 + assert await glide_client.spop(key) == member.encode() + + member2 = get_random_string(5) + member3 = get_random_string(5) + assert await glide_client.sadd(key, [member, member2, member3]) == 3 + assert await glide_client.spop_count(key, 4) == convert_string_to_bytes_object( + {member, member2, member3} + ) + + assert await glide_client.scard(key) == 0 - assert await redis_client.expire(key, 10) - assert await redis_client.persist(key) + assert await glide_client.spop("non_existing_key") == None + assert await glide_client.spop_count("non_existing_key", 3) == set() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_geoadd(self, redis_client: TRedisClient): - key, key2 = get_random_string(10), get_random_string(10) - members_coordinates = { - "Palermo": GeospatialData(13.361389, 38.115556), - "Catania": GeospatialData(15.087269, 37.502669), - } - assert await redis_client.geoadd(key, members_coordinates) == 2 - members_coordinates["Catania"].latitude = 39 - assert ( - await redis_client.geoadd( - key, - members_coordinates, - existing_options=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, - ) - == 0 + async def test_smove(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + assert await glide_client.sadd(key1, ["1", "2", "3"]) == 3 + assert await glide_client.sadd(key2, ["2", "3"]) == 2 + + # move an element + assert await glide_client.smove(key1, key2, "1") is True + assert await glide_client.smembers(key1) == convert_string_to_bytes_object( + {"2", "3"} ) - assert ( - await redis_client.geoadd( - key, - members_coordinates, - existing_options=ConditionalChange.ONLY_IF_EXISTS, - ) - == 0 + assert await glide_client.smembers(key2) == convert_string_to_bytes_object( + {"1", "2", "3"} ) - members_coordinates["Catania"].latitude = 40 - members_coordinates.update({"Tel-Aviv": GeospatialData(32.0853, 34.7818)}) - assert ( - await redis_client.geoadd( - key, - members_coordinates, - changed=True, - ) - == 2 + + # moved element already exists in the destination set + assert await glide_client.smove(key2, key1, "2") is True + assert await glide_client.smembers(key1) == convert_string_to_bytes_object( + {"2", "3"} + ) + assert await glide_client.smembers(key2) == convert_string_to_bytes_object( + {"1", "3"} ) - assert await redis_client.set(key2, "value") == OK - with pytest.raises(RequestError): - await redis_client.geoadd(key2, members_coordinates) + # attempt to move from a non-existing key + assert await glide_client.smove(non_existing_key, key1, "4") is False + assert await glide_client.smembers(key1) == convert_string_to_bytes_object( + {"2", "3"} + ) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_geoadd_invalid_args(self, redis_client: TRedisClient): - key = get_random_string(10) + # move to a new set + assert await glide_client.smove(key1, key3, "2") + assert await glide_client.smembers(key1) == {b"3"} + assert await glide_client.smembers(key3) == {b"2"} - with pytest.raises(RequestError): - await redis_client.geoadd(key, {}) + # attempt to move a missing element + assert await glide_client.smove(key1, key3, "42") is False + assert await glide_client.smembers(key1) == {b"3"} + assert await glide_client.smembers(key3) == {b"2"} - with pytest.raises(RequestError): - await redis_client.geoadd(key, {"Place": GeospatialData(-181, 0)}) + # move missing element to missing key + assert await glide_client.smove(key1, non_existing_key, "42") is False + assert await glide_client.smembers(key1) == {b"3"} + assert await glide_client.type(non_existing_key) == b"none" + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK with pytest.raises(RequestError): - await redis_client.geoadd(key, {"Place": GeospatialData(181, 0)}) + await glide_client.smove(string_key, key1, "_") + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sunion(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + member1_list: List[TEncodable] = ["a", "b", "c"] + member2_list: List[TEncodable] = ["b", "c", "d", "e"] + + assert await glide_client.sadd(key1, member1_list) == 3 + assert await glide_client.sadd(key2, member2_list) == 4 + assert await glide_client.sunion([key1, key2]) == {b"a", b"b", b"c", b"d", b"e"} + + # invalid argument - key list must not be empty with pytest.raises(RequestError): - await redis_client.geoadd(key, {"Place": GeospatialData(0, 86)}) + await glide_client.sunion([]) - with pytest.raises(RequestError): - await redis_client.geoadd(key, {"Place": GeospatialData(0, -86)}) + # non-existing key returns the set of existing keys + assert await glide_client.sunion( + [key1, non_existing_key] + ) == convert_string_to_bytes_object(set(cast(List[str], member1_list))) + + # non-set key + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError) as e: + await glide_client.sunion([key2]) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_geohash(self, redis_client: TRedisClient): - key = get_random_string(10) - members_coordinates = { - "Palermo": GeospatialData(13.361389, 38.115556), - "Catania": GeospatialData(15.087269, 37.502669), - } - assert await redis_client.geoadd(key, members_coordinates) == 2 - assert await redis_client.geohash(key, ["Palermo", "Catania", "Place"]) == [ - "sqc8b49rny0", - "sqdtr74hyu0", - None, - ] + async def test_sunionstore(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + key4 = f"{{testKey}}:4-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + assert await glide_client.sadd(key1, ["a", "b", "c"]) == 3 + assert await glide_client.sadd(key2, ["c", "d", "e"]) == 3 + assert await glide_client.sadd(key3, ["e", "f", "g"]) == 3 + + # store union in new key + assert await glide_client.sunionstore(key4, [key1, key2]) == 5 + assert await glide_client.smembers(key4) == convert_string_to_bytes_object( + {"a", "b", "c", "d", "e"} + ) - assert ( - await redis_client.geohash( - "non_existing_key", ["Palermo", "Catania", "Place"] - ) - == [None] * 3 + # overwrite existing set + assert await glide_client.sunionstore(key1, [key4, key2]) == 5 + assert await glide_client.smembers(key1) == convert_string_to_bytes_object( + {"a", "b", "c", "d", "e"} ) - # Neccessary to check since we are enforcing the user to pass a list of members while redis don't - # But when running the command with key only (and no members) the returned value will always be an empty list - # So in case of any changes, this test will fail and inform us that we should allow not passing any members. - assert await redis_client.geohash(key, []) == [] + # overwrite one of the source keys + assert await glide_client.sunionstore(key2, [key4, key2]) == 5 + assert await glide_client.smembers(key1) == convert_string_to_bytes_object( + {"a", "b", "c", "d", "e"} + ) + + # union with a non existing key + assert await glide_client.sunionstore(key2, [non_existing_key]) == 0 + assert await glide_client.smembers(key2) == set() - assert await redis_client.set(key, "value") == OK + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "value") == OK with pytest.raises(RequestError): - await redis_client.geohash(key, ["Palermo", "Catania"]) + await glide_client.sunionstore(key4, [string_key, key1]) + + # overwrite destination when destination is not a set + assert await glide_client.sunionstore(string_key, [key1, key3]) == 7 + assert await glide_client.smembers( + string_key + ) == convert_string_to_bytes_object( + { + "a", + "b", + "c", + "d", + "e", + "f", + "g", + } + ) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zadd_zaddincr(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 - assert await redis_client.zadd_incr(key, member="one", increment=2) == 3.0 + async def test_sinter(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + member1_list: List[TEncodable] = ["a", "b", "c"] + member2_list: List[TEncodable] = ["c", "d", "e"] + + # positive test case + assert await glide_client.sadd(key1, member1_list) == 3 + assert await glide_client.sadd(key2, member2_list) == 3 + assert await glide_client.sinter([key1, key2]) == {b"c"} + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.sinter([]) + + # non-existing key returns empty set + assert await glide_client.sinter([key1, non_existing_key]) == set() + + # non-set key + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError) as e: + await glide_client.sinter([key2]) + assert "Operation against a key holding the wrong kind of value" in str(e) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zadd_nx_xx(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert ( - await redis_client.zadd( - key, - members_scores=members_scores, - existing_options=ConditionalChange.ONLY_IF_EXISTS, - ) - == 0 - ) - assert ( - await redis_client.zadd( - key, - members_scores=members_scores, - existing_options=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, - ) - == 3 - ) + async def test_sinterstore(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + key3 = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + member1_list: List[TEncodable] = ["a", "b", "c"] + member2_list: List[TEncodable] = ["c", "d", "e"] + + assert await glide_client.sadd(key1, member1_list) == 3 + assert await glide_client.sadd(key2, member2_list) == 3 + + # store in new key + assert await glide_client.sinterstore(key3, [key1, key2]) == 1 + assert await glide_client.smembers(key3) == {b"c"} + + # overwrite existing set, which is also a source set + assert await glide_client.sinterstore(key2, [key2, key3]) == 1 + assert await glide_client.smembers(key2) == {b"c"} + + # source set is the same as the existing set + assert await glide_client.sinterstore(key2, [key2]) == 1 + assert await glide_client.smembers(key2) == {b"c"} + + # intersection with non-existing key + assert await glide_client.sinterstore(key1, [key2, non_existing_key]) == 0 + assert await glide_client.smembers(key1) == set() + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.sinterstore(key3, []) - assert ( - await redis_client.zadd_incr( - key, - member="one", - increment=5.0, - existing_options=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, - ) - is None - ) + # non-set key + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError) as e: + await glide_client.sinterstore(key3, [string_key]) - assert ( - await redis_client.zadd_incr( - key, - member="one", - increment=5.0, - existing_options=ConditionalChange.ONLY_IF_EXISTS, - ) - == 6.0 - ) + # overwrite non-set key + assert await glide_client.sinterstore(string_key, [key2]) == 1 + assert await glide_client.smembers(string_key) == {b"c"} @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zadd_gt_lt(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": -3, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 - members_scores["one"] = 10 + async def test_sintercard(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + key3 = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + member1_list: List[TEncodable] = ["a", "b", "c"] + member2_list: List[TEncodable] = ["b", "c", "d", "e"] + member3_list: List[TEncodable] = ["b", "c", "f", "g"] + + assert await glide_client.sadd(key1, member1_list) == 3 + assert await glide_client.sadd(key2, member2_list) == 4 + assert await glide_client.sadd(key3, member3_list) == 4 + + # Basic intersection assert ( - await redis_client.zadd( - key, - members_scores=members_scores, - update_condition=UpdateOptions.GREATER_THAN, - changed=True, - ) - == 1 - ) + await glide_client.sintercard([key1, key2]) == 2 + ) # Intersection of key1 and key2 is {"b", "c"} + # Intersection with non-existing key assert ( - await redis_client.zadd( - key, - members_scores=members_scores, - update_condition=UpdateOptions.LESS_THAN, - changed=True, - ) - == 0 - ) + await glide_client.sintercard([key1, non_existing_key]) == 0 + ) # No common elements - assert ( - await redis_client.zadd_incr( - key, - member="one", - increment=-3.0, - update_condition=UpdateOptions.LESS_THAN, - ) - == 7.0 - ) + # Intersection with a single key + assert await glide_client.sintercard([key1]) == 3 # All elements in key1 + # Intersection with limit assert ( - await redis_client.zadd_incr( - key, - member="one", - increment=-3.0, - update_condition=UpdateOptions.GREATER_THAN, - ) - is None - ) - - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zrem(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 + await glide_client.sintercard([key1, key2, key3], limit=1) == 1 + ) # Stops early at limit - assert await redis_client.zrem(key, ["one"]) == 1 - assert await redis_client.zrem(key, ["one", "two", "three"]) == 2 + # Invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.sintercard([]) - assert await redis_client.zrem("non_existing_set", ["member"]) == 0 + # Non-set key + assert await glide_client.set(string_key, "value") == "OK" + with pytest.raises(RequestError): + await glide_client.sintercard([string_key]) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zremrangebyscore(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores) == 3 + async def test_sdiff(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" - assert ( - await redis_client.zremrangebyscore( - key, ScoreBoundary(1, False), ScoreBoundary(2) - ) - == 1 - ) - assert ( - await redis_client.zremrangebyscore(key, ScoreBoundary(1), InfBound.NEG_INF) - == 0 + assert await glide_client.sadd(key1, ["a", "b", "c"]) == 3 + assert await glide_client.sadd(key2, ["c", "d", "e"]) == 3 + + assert await glide_client.sdiff([key1, key2]) == convert_string_to_bytes_object( + {"a", "b"} ) - assert ( - await redis_client.zremrangebyscore( - "non_existing_set", InfBound.NEG_INF, InfBound.POS_INF - ) - == 0 + assert await glide_client.sdiff([key2, key1]) == convert_string_to_bytes_object( + {"d", "e"} ) - assert await redis_client.set(key, "value") == OK + assert await glide_client.sdiff( + [key1, non_existing_key] + ) == convert_string_to_bytes_object({"a", "b", "c"}) + assert await glide_client.sdiff([non_existing_key, key1]) == set() + + # invalid argument - key list must not be empty with pytest.raises(RequestError): - await redis_client.zremrangebyscore(key, InfBound.NEG_INF, InfBound.POS_INF) + await glide_client.sdiff([]) + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.sdiff([string_key]) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zremrangebylex(self, redis_client: TRedisClient): - key1 = get_random_string(10) - key2 = get_random_string(10) - range = RangeByIndex(0, -1) - members_scores = {"a": 1, "b": 2, "c": 3, "d": 4} - assert await redis_client.zadd(key1, members_scores) == 4 - - assert ( - await redis_client.zremrangebylex( - key1, LexBoundary("a", False), LexBoundary("c") - ) - == 2 + async def test_sdiffstore(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + assert await glide_client.sadd(key1, ["a", "b", "c"]) == 3 + assert await glide_client.sadd(key2, ["c", "d", "e"]) == 3 + + # Store diff in new key + assert await glide_client.sdiffstore(key3, [key1, key2]) == 2 + assert await glide_client.smembers(key3) == convert_string_to_bytes_object( + {"a", "b"} ) - assert await redis_client.zrange_withscores(key1, range) == {"a": 1.0, "d": 4.0} - assert ( - await redis_client.zremrangebylex(key1, LexBoundary("d"), InfBound.POS_INF) - == 1 + # Overwrite existing set + assert await glide_client.sdiffstore(key3, [key2, key1]) == 2 + assert await glide_client.smembers(key3) == convert_string_to_bytes_object( + {"d", "e"} ) - assert await redis_client.zrange_withscores(key1, range) == {"a": 1.0} - # min_lex > max_lex - assert ( - await redis_client.zremrangebylex(key1, LexBoundary("a"), InfBound.NEG_INF) - == 0 + # Overwrite one of the source sets + assert await glide_client.sdiffstore(key3, [key2, key3]) == 1 + assert await glide_client.smembers(key3) == {b"c"} + + # Diff between non-empty set and empty set + assert await glide_client.sdiffstore(key3, [key1, non_existing_key]) == 3 + assert await glide_client.smembers(key3) == convert_string_to_bytes_object( + {"a", "b", "c"} ) - assert await redis_client.zrange_withscores(key1, range) == {"a": 1.0} - assert ( - await redis_client.zremrangebylex( - "non_existing_key", InfBound.NEG_INF, InfBound.POS_INF - ) - == 0 - ) + # Diff between empty set and non-empty set + assert await glide_client.sdiffstore(key3, [non_existing_key, key1]) == 0 + assert await glide_client.smembers(key3) == set() - # key exists, but it is not a sorted set - assert await redis_client.set(key2, "value") == OK + # invalid argument - key list must not be empty with pytest.raises(RequestError): - await redis_client.zremrangebylex( - key2, LexBoundary("a", False), LexBoundary("c") - ) + await glide_client.sdiffstore(key3, []) + + # source key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.sdiffstore(key3, [string_key]) + + # Overwrite a key holding a non-set value + assert await glide_client.sdiffstore(string_key, [key1, key2]) == 2 + assert await glide_client.smembers( + string_key + ) == convert_string_to_bytes_object({"a", "b"}) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zlexcount(self, redis_client: TRedisClient): + async def test_smismember(self, glide_client: TGlideClient): key1 = get_random_string(10) - key2 = get_random_string(10) - members_scores = {"a": 1.0, "b": 2.0, "c": 3.0} + string_key = get_random_string(10) + non_existing_key = get_random_string(10) - assert await redis_client.zadd(key1, members_scores) == 3 - assert ( - await redis_client.zlexcount(key1, InfBound.NEG_INF, InfBound.POS_INF) == 3 - ) - assert ( - await redis_client.zlexcount( - key1, - LexBoundary("a", is_inclusive=False), - LexBoundary("c", is_inclusive=True), - ) - == 2 + assert await glide_client.sadd(key1, ["one", "two"]) == 2 + assert await glide_client.smismember(key1, ["two", "three"]) == [True, False] + + assert await glide_client.smismember(non_existing_key, ["two"]) == [False] + + # invalid argument - member list must not be empty + with pytest.raises(RequestError): + await glide_client.smismember(key1, []) + + # source key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.smismember(string_key, ["two"]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_ltrim(self, glide_client: TGlideClient): + key = get_random_string(10) + value_list: List[TEncodable] = ["value4", "value3", "value2", "value1"] + + assert await glide_client.lpush(key, value_list) == 4 + assert await glide_client.ltrim(key, 0, 1) == OK + assert await glide_client.lrange(key, 0, -1) == convert_string_to_bytes_object( + ["value1", "value2"] ) - assert ( - await redis_client.zlexcount( - key1, InfBound.NEG_INF, LexBoundary("c", is_inclusive=True) - ) - == 3 + + assert await glide_client.ltrim(key, 4, 2) == OK + assert await glide_client.lrange(key, 0, -1) == [] + + assert await glide_client.ltrim("non_existing_key", 0, 1) == OK + + assert await glide_client.set(key, "foo") == OK + with pytest.raises(RequestError) as e: + await glide_client.ltrim(key, 0, 1) + assert "Operation against a key holding the wrong kind of value" in str(e) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lrem(self, glide_client: TGlideClient): + key = get_random_string(10) + value_list: List[TEncodable] = [ + "value1", + "value2", + "value1", + "value1", + "value2", + ] + + assert await glide_client.lpush(key, value_list) == 5 + + assert await glide_client.lrem(key, 2, "value1") == 2 + assert await glide_client.lrange(key, 0, -1) == convert_string_to_bytes_object( + ["value2", "value2", "value1"] ) - # Incorrect range; start > end - assert ( - await redis_client.zlexcount( - key1, InfBound.POS_INF, LexBoundary("c", is_inclusive=True) - ) - == 0 + + assert await glide_client.lrem(key, -1, "value2") == 1 + assert await glide_client.lrange(key, 0, -1) == convert_string_to_bytes_object( + ["value2", "value1"] ) - assert ( - await redis_client.zlexcount( - "non_existing_key", InfBound.NEG_INF, InfBound.POS_INF + + assert await glide_client.lrem(key, 0, "value2") == 1 + assert await glide_client.lrange(key, 0, -1) == [b"value1"] + + assert await glide_client.lrem("non_existing_key", 2, "value") == 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_llen(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + value_list: List[TEncodable] = ["value4", "value3", "value2", "value1"] + + assert await glide_client.lpush(key1, value_list) == 4 + assert await glide_client.llen(key1) == 4 + + assert await glide_client.llen("non_existing_key") == 0 + + assert await glide_client.set(key2, "foo") == OK + with pytest.raises(RequestError) as e: + await glide_client.llen(key2) + assert "Operation against a key holding the wrong kind of value" in str(e) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_strlen(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + value_list: List[TEncodable] = ["value4", "value3", "value2", "value1"] + + assert await glide_client.set(key1, "foo") == OK + assert await glide_client.strlen(key1) == 3 + assert await glide_client.strlen("non_existing_key") == 0 + + assert await glide_client.lpush(key2, value_list) == 4 + with pytest.raises(RequestError): + assert await glide_client.strlen(key2) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_rename(self, glide_client: TGlideClient): + key1 = "{" + get_random_string(10) + "}" + assert await glide_client.set(key1, "foo") == OK + assert await glide_client.rename(key1, key1 + "_rename") == OK + assert await glide_client.exists([key1 + "_rename"]) == 1 + + with pytest.raises(RequestError): + assert await glide_client.rename( + "{same_slot}" + "non_existing_key", "{same_slot}" + "_rename" ) - == 0 - ) - # key exists, but it is not a sorted set - assert await redis_client.set(key2, "value") == OK + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_renamenx(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + # Verify that attempting to rename a non-existing key throws an error with pytest.raises(RequestError): - await redis_client.zlexcount(key2, InfBound.NEG_INF, InfBound.POS_INF) + assert await glide_client.renamenx(non_existing_key, key1) + + # Test RENAMENX with string values + assert await glide_client.set(key1, "key1") == OK + assert await glide_client.set(key3, "key3") == OK + # Test that RENAMENX can rename key1 to key2 (where key2 does not yet exist) + assert await glide_client.renamenx(key1, key2) is True + # Verify that key2 now holds the value that was in key1 + assert await glide_client.get(key2) == b"key1" + # Verify that RENAMENX doesn't rename key2 to key3, since key3 already exists + assert await glide_client.renamenx(key2, key3) is False + # Verify that key3 remains unchanged + assert await glide_client.get(key3) == b"key3" + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_exists(self, glide_client: TGlideClient): + keys = [get_random_string(10), get_random_string(10)] + + assert await glide_client.set(keys[0], "value") == OK + assert await glide_client.exists(keys) == 1 + + assert await glide_client.set(keys[1], "value") == OK + assert await glide_client.exists(keys) == 2 + keys.append("non_existing_key") + assert await glide_client.exists(keys) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_unlink(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + key3 = get_random_string(10) + + assert await glide_client.set(key1, "value") == OK + assert await glide_client.set(key2, "value") == OK + assert await glide_client.set(key3, "value") == OK + assert await glide_client.unlink([key1, key2, "non_existing_key", key3]) == 3 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zcard(self, redis_client: TRedisClient): + async def test_expire_pexpire_ttl_expiretime_pexpiretime_with_positive_timeout( + self, glide_client: TGlideClient + ): key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 - assert await redis_client.zcard(key) == 3 + assert await glide_client.set(key, "foo") == OK + assert await glide_client.ttl(key) == -1 + + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -1 + assert await glide_client.pexpiretime(key) == -1 + + assert await glide_client.expire(key, 10) == 1 + assert await glide_client.ttl(key) in range(11) + + # set command clears the timeout. + assert await glide_client.set(key, "bar") == OK + if await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.pexpire(key, 10000) + else: + assert await glide_client.pexpire(key, 10000, ExpireOptions.HasNoExpiry) + assert await glide_client.ttl(key) in range(11) - assert await redis_client.zrem(key, ["one"]) == 1 - assert await redis_client.zcard(key) == 2 - assert await redis_client.zcard("non_existing_key") == 0 + if await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expire(key, 15) + else: + assert await glide_client.expire(key, 15, ExpireOptions.HasExistingExpiry) + assert await glide_client.expiretime(key) > int(time.time()) + assert await glide_client.pexpiretime(key) > (int(time.time()) * 1000) + assert await glide_client.ttl(key) in range(16) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zcount(self, redis_client: TRedisClient): + async def test_expireat_pexpireat_ttl_with_positive_timeout( + self, glide_client: TGlideClient + ): key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 + assert await glide_client.set(key, "foo") == OK + current_time = int(time.time()) - assert await redis_client.zcount(key, InfBound.NEG_INF, InfBound.POS_INF) == 3 - assert ( - await redis_client.zcount( - key, - ScoreBoundary(1, is_inclusive=False), - ScoreBoundary(3, is_inclusive=False), + assert await glide_client.expireat(key, current_time + 10) == 1 + assert await glide_client.ttl(key) in range(11) + if await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expireat(key, current_time + 50) == 1 + else: + assert ( + await glide_client.expireat( + key, current_time + 50, ExpireOptions.NewExpiryGreaterThanCurrent + ) + == 1 ) - == 1 - ) - assert ( - await redis_client.zcount( - key, - ScoreBoundary(1, is_inclusive=False), - ScoreBoundary(3, is_inclusive=True), + assert await glide_client.ttl(key) in range(51) + + # set command clears the timeout. + assert await glide_client.set(key, "bar") == OK + current_time_ms = int(time.time() * 1000) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert not await glide_client.pexpireat( + key, current_time_ms + 50000, ExpireOptions.HasExistingExpiry ) - == 2 - ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_expire_pexpire_expireat_pexpireat_expiretime_pexpiretime_past_or_negative_timeout( + self, glide_client: TGlideClient + ): + key = get_random_string(10) + assert await glide_client.set(key, "foo") == OK + assert await glide_client.ttl(key) == -1 + + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -1 + assert await glide_client.pexpiretime(key) == -1 + + assert await glide_client.expire(key, -10) is True + assert await glide_client.ttl(key) == -2 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -2 + assert await glide_client.pexpiretime(key) == -2 + + assert await glide_client.set(key, "foo") == OK + assert await glide_client.pexpire(key, -10000) + assert await glide_client.ttl(key) == -2 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -2 + assert await glide_client.pexpiretime(key) == -2 + + assert await glide_client.set(key, "foo") == OK + assert await glide_client.expireat(key, int(time.time()) - 50) == 1 + assert await glide_client.ttl(key) == -2 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -2 + assert await glide_client.pexpiretime(key) == -2 + + assert await glide_client.set(key, "foo") == OK + assert await glide_client.pexpireat(key, int(time.time() * 1000) - 50000) + assert await glide_client.ttl(key) == -2 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -2 + assert await glide_client.pexpiretime(key) == -2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_expire_pexpire_expireAt_pexpireAt_ttl_expiretime_pexpiretime_non_existing_key( + self, glide_client: TGlideClient + ): + key = get_random_string(10) + + assert await glide_client.expire(key, 10) == 0 + assert not await glide_client.pexpire(key, 10000) + assert await glide_client.expireat(key, int(time.time()) + 50) == 0 + assert not await glide_client.pexpireat(key, int(time.time() * 1000) + 50000) + assert await glide_client.ttl(key) == -2 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.expiretime(key) == -2 + assert await glide_client.pexpiretime(key) == -2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_pttl(self, glide_client: TGlideClient): + key = get_random_string(10) + assert await glide_client.pttl(key) == -2 + current_time = int(time.time()) + + assert await glide_client.set(key, "value") == OK + assert await glide_client.pttl(key) == -1 + + assert await glide_client.expire(key, 10) + assert 0 < await glide_client.pttl(key) <= 10000 + + assert await glide_client.expireat(key, current_time + 20) + assert 0 < await glide_client.pttl(key) <= 20000 + + assert await glide_client.pexpireat(key, current_time * 1000 + 30000) + assert 0 < await glide_client.pttl(key) <= 30000 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_persist(self, glide_client: TGlideClient): + key = get_random_string(10) + assert await glide_client.set(key, "value") == OK + assert not await glide_client.persist(key) + + assert await glide_client.expire(key, 10) + assert await glide_client.persist(key) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geoadd(self, glide_client: TGlideClient): + key, key2 = get_random_string(10), get_random_string(10) + members_coordinates: Dict[str | bytes, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + } + assert await glide_client.geoadd(key, members_coordinates) == 2 + members_coordinates["Catania"].latitude = 39 assert ( - await redis_client.zcount( - key, InfBound.NEG_INF, ScoreBoundary(3, is_inclusive=True) + await glide_client.geoadd( + key, + members_coordinates, + existing_options=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, ) - == 3 + == 0 ) assert ( - await redis_client.zcount( - key, InfBound.POS_INF, ScoreBoundary(3, is_inclusive=True) + await glide_client.geoadd( + key, + members_coordinates, + existing_options=ConditionalChange.ONLY_IF_EXISTS, ) == 0 ) + members_coordinates["Catania"].latitude = 40 + members_coordinates.update({"Tel-Aviv": GeospatialData(32.0853, 34.7818)}) assert ( - await redis_client.zcount( - "non_existing_key", InfBound.NEG_INF, InfBound.POS_INF + await glide_client.geoadd( + key, + members_coordinates, + changed=True, ) - == 0 + == 2 ) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zscore(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 - assert await redis_client.zscore(key, "one") == 1.0 - - assert await redis_client.zscore(key, "non_existing_member") is None - assert ( - await redis_client.zscore("non_existing_key", "non_existing_member") is None - ) + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.geoadd(key2, members_coordinates) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zpopmin(self, redis_client: TRedisClient): + async def test_geoadd_invalid_args(self, glide_client: TGlideClient): key = get_random_string(10) - members_scores = {"a": 1.0, "b": 2.0, "c": 3.0} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 - assert await redis_client.zpopmin(key) == {"a": 1.0} - assert await redis_client.zpopmin(key, 3) == {"b": 2.0, "c": 3.0} - assert await redis_client.zpopmin(key) == {} - assert await redis_client.set(key, "value") == OK with pytest.raises(RequestError): - await redis_client.zpopmin(key) + await glide_client.geoadd(key, {}) - assert await redis_client.zpopmin("non_exisitng_key") == {} + with pytest.raises(RequestError): + await glide_client.geoadd(key, {"Place": GeospatialData(-181, 0)}) + + with pytest.raises(RequestError): + await glide_client.geoadd(key, {"Place": GeospatialData(181, 0)}) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zpopmax(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"a": 1.0, "b": 2.0, "c": 3.0} - assert await redis_client.zadd(key, members_scores) == 3 - assert await redis_client.zpopmax(key) == {"c": 3.0} - assert await redis_client.zpopmax(key, 3) == {"b": 2.0, "a": 1.0} - assert await redis_client.zpopmax(key) == {} - assert await redis_client.set(key, "value") == OK with pytest.raises(RequestError): - await redis_client.zpopmax(key) + await glide_client.geoadd(key, {"Place": GeospatialData(0, 86)}) - assert await redis_client.zpopmax("non_exisitng_key") == {} + with pytest.raises(RequestError): + await glide_client.geoadd(key, {"Place": GeospatialData(0, -86)}) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zrange_by_index(self, redis_client: TRedisClient): + async def test_geosearch_by_box(self, glide_client: TGlideClient): key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 - - assert await redis_client.zrange(key, RangeByIndex(start=0, stop=1)) == [ - "one", - "two", + members = ["Catania", "Palermo", "edge2", "edge1"] + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + "edge1": GeospatialData(12.758489, 38.788135), + "edge2": GeospatialData(17.241510, 38.788135), + } + result = [ + [ + "Catania", + [56.4413, 3479447370796909, [15.087267458438873, 37.50266842333162]], + ], + [ + "Palermo", + [190.4424, 3479099956230698, [13.361389338970184, 38.1155563954963]], + ], + [ + "edge2", + [279.7403, 3481342659049484, [17.241510450839996, 38.78813451624225]], + ], + [ + "edge1", + [279.7405, 3479273021651468, [12.75848776102066, 38.78813451624225]], + ], ] + assert await glide_client.geoadd(key, members_coordinates) == 4 - assert ( - await redis_client.zrange_withscores(key, RangeByIndex(start=0, stop=-1)) - ) == {"one": 1.0, "two": 2.0, "three": 3.0} + # Test search by box, unit: kilometers, from a geospatial data + assert await glide_client.geosearch( + key, + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + OrderBy.ASC, + ) == convert_string_to_bytes_object(members) - assert await redis_client.zrange( - key, RangeByIndex(start=0, stop=1), reverse=True - ) == [ - "three", - "two", - ] + assert await glide_client.geosearch( + key, + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + OrderBy.DESC, + with_coord=True, + with_dist=True, + with_hash=True, + ) == convert_string_to_bytes_object(result[::-1]) + + assert await glide_client.geosearch( + key, + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + OrderBy.ASC, + count=GeoSearchCount(1), + with_dist=True, + with_hash=True, + ) == [[b"Catania", [56.4413, 3479447370796909]]] + + # Test search by box, unit: meters, from a member, with distance + meters = 400 * 1000 + assert await glide_client.geosearch( + key, + "Catania", + GeoSearchByBox(meters, meters, GeoUnit.METERS), + OrderBy.DESC, + with_dist=True, + ) == convert_string_to_bytes_object( + [["edge2", [236529.1799]], ["Palermo", [166274.1516]], ["Catania", [0.0]]] + ) - assert await redis_client.zrange(key, RangeByIndex(start=3, stop=1)) == [] + # Test search by box, unit: feet, from a member, with limited count to 2, with hash + feet = 400 * 3280.8399 + assert await glide_client.geosearch( + key, + "Palermo", + GeoSearchByBox(feet, feet, GeoUnit.FEET), + OrderBy.ASC, + count=GeoSearchCount(2), + with_hash=True, + ) == [[b"Palermo", [3479099956230698]], [b"edge1", [3479273021651468]]] + + # Test search by box, unit: miles, from a geospatial data, with limited ANY count to 1 assert ( - await redis_client.zrange_withscores(key, RangeByIndex(start=3, stop=1)) - == {} - ) + await glide_client.geosearch( + key, + GeospatialData(15, 37), + GeoSearchByBox(250, 250, GeoUnit.MILES), + OrderBy.ASC, + count=GeoSearchCount(1, True), + ) + )[0] in cast(list, convert_string_to_bytes_object(members)) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zrange_byscore(self, redis_client: TRedisClient): + async def test_geosearch_by_radius(self, glide_client: TGlideClient): key = get_random_string(10) - members_scores = {"one": 1, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + "edge1": GeospatialData(12.758489, 38.788135), + "edge2": GeospatialData(17.241510, 38.788135), + } + result = [ + [ + "Catania", + [56.4413, 3479447370796909, [15.087267458438873, 37.50266842333162]], + ], + [ + "Palermo", + [190.4424, 3479099956230698, [13.361389338970184, 38.1155563954963]], + ], + ] + members = ["Catania", "Palermo", "edge2", "edge1"] + assert await glide_client.geoadd(key, members_coordinates) == 4 - assert await redis_client.zrange( + # Test search by radius, units: feet, from a member + feet = 200 * 3280.8399 + assert await glide_client.geosearch( key, - RangeByScore( - start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) - ), - ) == ["one", "two"] - - assert ( - await redis_client.zrange_withscores( - key, - RangeByScore(start=InfBound.NEG_INF, stop=InfBound.POS_INF), - ) - ) == {"one": 1.0, "two": 2.0, "three": 3.0} + "Catania", + GeoSearchByRadius(feet, GeoUnit.FEET), + OrderBy.ASC, + ) == convert_string_to_bytes_object(members[:2]) + + # Test search by radius, units: meters, from a member + meters = 200 * 1000 + assert await glide_client.geosearch( + key, + "Catania", + GeoSearchByRadius(meters, GeoUnit.METERS), + OrderBy.DESC, + ) == convert_string_to_bytes_object(members[:2][::-1]) - assert await redis_client.zrange( + # Test search by radius, unit: miles, from a geospatial data + assert await glide_client.geosearch( key, - RangeByScore( - start=ScoreBoundary(3, is_inclusive=False), stop=InfBound.NEG_INF - ), - reverse=True, - ) == ["two", "one"] + GeospatialData(15, 37), + GeoSearchByRadius(175, GeoUnit.MILES), + OrderBy.DESC, + ) == convert_string_to_bytes_object(members[::-1]) + # Test search by radius, unit: kilometers, from a geospatial data, with limited count to 2 + assert await glide_client.geosearch( + key, + GeospatialData(15, 37), + GeoSearchByRadius(200, GeoUnit.KILOMETERS), + OrderBy.ASC, + count=GeoSearchCount(2), + with_coord=True, + with_dist=True, + with_hash=True, + ) == convert_string_to_bytes_object(result) + + # Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 assert ( - await redis_client.zrange( + await glide_client.geosearch( key, - RangeByScore( - start=InfBound.NEG_INF, - stop=InfBound.POS_INF, - limit=Limit(offset=1, count=2), - ), + GeospatialData(15, 37), + GeoSearchByRadius(200, GeoUnit.KILOMETERS), + OrderBy.ASC, + count=GeoSearchCount(1, True), + with_coord=True, + with_dist=True, + with_hash=True, ) - ) == ["two", "three"] + )[0] in cast(list, convert_string_to_bytes_object(result)) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geosearch_no_result(self, glide_client: TGlideClient): + key = get_random_string(10) + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + "edge1": GeospatialData(12.758489, 38.788135), + "edge2": GeospatialData(17.241510, 38.788135), + } + assert await glide_client.geoadd(key, members_coordinates) == 4 + + # No membes within the aea assert ( - await redis_client.zrange( + await glide_client.geosearch( key, - RangeByScore( - start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) - ), - reverse=True, + GeospatialData(15, 37), + GeoSearchByBox(50, 50, GeoUnit.METERS), + OrderBy.ASC, ) == [] - ) # stop is greater than start with reverse set to True + ) assert ( - await redis_client.zrange( + await glide_client.geosearch( key, - RangeByScore( - start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) - ), + GeospatialData(15, 37), + GeoSearchByRadius(10, GeoUnit.METERS), + OrderBy.ASC, ) == [] - ) # start is greater than stop + ) + + # No members in the area (apart from the member we seach fom itself) + assert await glide_client.geosearch( + key, + "Catania", + GeoSearchByBox(10, 10, GeoUnit.KILOMETERS), + ) == [b"Catania"] + + assert await glide_client.geosearch( + key, + "Catania", + GeoSearchByRadius(10, GeoUnit.METERS), + ) == [b"Catania"] + + # Search from non exiting memeber + with pytest.raises(RequestError): + await glide_client.geosearch( + key, + "non_existing_member", + GeoSearchByBox(10, 10, GeoUnit.MILES), + ) + + assert await glide_client.set(key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.geosearch( + key, + "Catania", + GeoSearchByBox(10, 10, GeoUnit.MILES), + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geosearchstore_by_box(self, glide_client: TGlideClient): + key = f"{{testKey}}:{get_random_string(10)}" + destination_key = f"{{testKey}}:{get_random_string(8)}" + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + "edge1": GeospatialData(12.758489, 38.788135), + "edge2": GeospatialData(17.241510, 38.788135), + } + result = { + b"Catania": [56.4412578701582, 3479447370796909.0], + b"Palermo": [190.44242984775784, 3479099956230698.0], + b"edge2": [279.7403417843143, 3481342659049484.0], + b"edge1": [279.7404521356343, 3479273021651468.0], + } + assert await glide_client.geoadd(key, members_coordinates) == 4 + # Test storing results of a box search, unit: kilometes, from a geospatial data assert ( - await redis_client.zrange_withscores( + await glide_client.geosearchstore( + destination_key, key, - RangeByScore( - start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) - ), + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), ) - == {} - ) # start is greater than stop + ) == 4 # Number of elements stored + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + expected_map = {member: value[1] for member, value in result.items()} + sorted_expected_map = dict(sorted(expected_map.items(), key=lambda x: x[1])) + assert compare_maps(zrange_map, sorted_expected_map) is True + # Test storing results of a box search, unit: kilometes, from a geospatial data, with distance assert ( - await redis_client.zrange_withscores( + await glide_client.geosearchstore( + destination_key, key, - RangeByScore( - start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) - ), - reverse=True, + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + store_dist=True, + ) + ) == 4 # Number of elements stored + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + expected_map = {member: value[0] for member, value in result.items()} + sorted_expected_map = dict(sorted(expected_map.items(), key=lambda x: x[1])) + assert compare_maps(zrange_map, sorted_expected_map) is True + + # Test storing results of a box search, unit: kilometes, from a geospatial data, with count + assert ( + await glide_client.geosearchstore( + destination_key, + key, + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + count=GeoSearchCount(1), + ) + ) == 1 # Number of elements stored + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_map, {b"Catania": 3479447370796909.0}) is True + + # Test storing results of a box search, unit: meters, from a member, with distance + meters = 400 * 1000 + assert ( + await glide_client.geosearchstore( + destination_key, + key, + "Catania", + GeoSearchByBox(meters, meters, GeoUnit.METERS), + store_dist=True, + ) + ) == 3 # Number of elements stored + + # Verify the stored results with distances + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + expected_distances = { + b"Catania": 0.0, + b"Palermo": 166274.15156960033, + b"edge2": 236529.17986494553, + } + assert compare_maps(zrange_map, expected_distances) is True + + # Test search by box, unit: feet, from a member, with limited ANY count to 2, with hash + feet = 400 * 3280.8399 + assert ( + await glide_client.geosearchstore( + destination_key, + key, + "Palermo", + GeoSearchByBox(feet, feet, GeoUnit.FEET), + count=GeoSearchCount(2), + ) + == 2 + ) + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + for member in zrange_map: + assert member in result + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geosearchstore_by_radius(self, glide_client: TGlideClient): + key = f"{{testKey}}:{get_random_string(10)}" + destination_key = f"{{testKey}}:{get_random_string(8)}" + # Checking when parts of the value contain bytes + members_coordinates: Mapping[TEncodable, GeospatialData] = { + b"Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + b"edge1": GeospatialData(12.758489, 38.788135), + "edge2": GeospatialData(17.241510, 38.788135), + } + result = { + b"Catania": [56.4412578701582, 3479447370796909.0], + b"Palermo": [190.44242984775784, 3479099956230698.0], + } + assert await glide_client.geoadd(key, members_coordinates) == 4 + + # Test storing results of a radius search, unit: feet, from a member + feet = 200 * 3280.8399 + assert ( + await glide_client.geosearchstore( + destination_key, + key, + "Catania", + GeoSearchByRadius(feet, GeoUnit.FEET), + ) + == 2 + ) + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + expected_map = {member: value[1] for member, value in result.items()} + sorted_expected_map = dict(sorted(expected_map.items(), key=lambda x: x[1])) + assert compare_maps(zrange_map, sorted_expected_map) is True + + # Test search by radius, units: meters, from a member + meters = 200 * 1000 + assert ( + await glide_client.geosearchstore( + destination_key, + key, + "Catania", + GeoSearchByRadius(meters, GeoUnit.METERS), + store_dist=True, + ) + == 2 + ) + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + expected_distances = { + b"Catania": 0.0, + b"Palermo": 166274.15156960033, + } + assert compare_maps(zrange_map, expected_distances) is True + + # Test search by radius, unit: miles, from a geospatial data + assert ( + await glide_client.geosearchstore( + destination_key, + key, + GeospatialData(15, 37), + GeoSearchByRadius(175, GeoUnit.MILES), + ) + == 4 + ) + + # Test storing results of a radius search, unit: kilometers, from a geospatial data, with limited count to 2 + kilometers = 200 + assert ( + await glide_client.geosearchstore( + destination_key, + key, + GeospatialData(15, 37), + GeoSearchByRadius(kilometers, GeoUnit.KILOMETERS), + count=GeoSearchCount(2), + store_dist=True, + ) + == 2 + ) + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + expected_map = {member: value[0] for member, value in result.items()} + sorted_expected_map = dict(sorted(expected_map.items(), key=lambda x: x[1])) + assert compare_maps(zrange_map, sorted_expected_map) is True + + # Test storing results of a radius search, unit: kilometers, from a geospatial data, with limited ANY count to 1 + assert ( + await glide_client.geosearchstore( + destination_key, + key, + GeospatialData(15, 37), + GeoSearchByRadius(kilometers, GeoUnit.KILOMETERS), + count=GeoSearchCount(1, True), + ) + == 1 + ) + + # Verify the stored results + zrange_map = await glide_client.zrange_withscores( + destination_key, RangeByIndex(0, -1) + ) + + for member in zrange_map: + assert member in result + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geosearchstore_no_result(self, glide_client: TGlideClient): + key = f"{{testKey}}:{get_random_string(10)}" + destination_key = f"{{testKey}}:{get_random_string(8)}" + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + "edge1": GeospatialData(12.758489, 38.788135), + "edge2": GeospatialData(17.241510, 38.788135), + } + assert await glide_client.geoadd(key, members_coordinates) == 4 + + # No members within the area + assert ( + await glide_client.geosearchstore( + destination_key, + key, + GeospatialData(15, 37), + GeoSearchByBox(50, 50, GeoUnit.METERS), + ) + == 0 + ) + + assert ( + await glide_client.geosearchstore( + destination_key, + key, + GeospatialData(15, 37), + GeoSearchByRadius(10, GeoUnit.METERS), + ) + == 0 + ) + + # No members in the area (apart from the member we search from itself) + assert ( + await glide_client.geosearchstore( + destination_key, + key, + "Catania", + GeoSearchByBox(10, 10, GeoUnit.KILOMETERS), + ) + == 1 + ) + + assert ( + await glide_client.geosearchstore( + destination_key, + key, + "Catania", + GeoSearchByRadius(10, GeoUnit.METERS), + ) + == 1 + ) + + # Search from non-existing member + with pytest.raises(RequestError): + await glide_client.geosearchstore( + destination_key, + key, + "non_existing_member", + GeoSearchByBox(10, 10, GeoUnit.MILES), + ) + + assert await glide_client.set(key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.geosearchstore( + destination_key, + key, + "Catania", + GeoSearchByBox(10, 10, GeoUnit.MILES), ) - == {} - ) # stop is greater than start with reverse set to True @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zrange_bylex(self, redis_client: TRedisClient): + async def test_geohash(self, glide_client: TGlideClient): key = get_random_string(10) - members_scores = {"a": 1, "b": 2, "c": 3} - assert await redis_client.zadd(key, members_scores=members_scores) == 3 + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + } + assert await glide_client.geoadd(key, members_coordinates) == 2 + assert await glide_client.geohash( + key, ["Palermo", "Catania", "Place"] + ) == convert_string_to_bytes_object( + [ + "sqc8b49rny0", + "sqdtr74hyu0", + None, + ] + ) - assert await redis_client.zrange( - key, - RangeByLex( - start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) - ), - ) == ["a", "b"] + assert ( + await glide_client.geohash( + "non_existing_key", ["Palermo", "Catania", "Place"] + ) + == [None] * 3 + ) + + # Neccessary to check since we are enforcing the user to pass a list of members while valkey don't + # But when running the command with key only (and no members) the returned value will always be an empty list + # So in case of any changes, this test will fail and inform us that we should allow not passing any members. + assert await glide_client.geohash(key, []) == [] + + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.geohash(key, ["Palermo", "Catania"]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geodist(self, glide_client: TGlideClient): + key, key2 = get_random_string(10), get_random_string(10) + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + } + assert await glide_client.geoadd(key, members_coordinates) == 2 + + assert await glide_client.geodist(key, "Palermo", "Catania") == 166274.1516 + assert ( + await glide_client.geodist(key, "Palermo", "Catania", GeoUnit.KILOMETERS) + == 166.2742 + ) + assert await glide_client.geodist(key, "Palermo", "Palermo", GeoUnit.MILES) == 0 + assert ( + await glide_client.geodist( + key, "Palermo", "non-existing-member", GeoUnit.FEET + ) + == None + ) + + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.geodist(key2, "Palmero", "Catania") + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_geopos(self, glide_client: TGlideClient): + key = get_random_string(10) + members_coordinates: Mapping[TEncodable, GeospatialData] = { + "Palermo": GeospatialData(13.361389, 38.115556), + "Catania": GeospatialData(15.087269, 37.502669), + } + assert await glide_client.geoadd(key, members_coordinates) == 2 + + # The comparison allows for a small tolerance level due to potential precision errors in floating-point calculations + # No worries, Python can handle it, therefore, this shouldn't fail + positions = await glide_client.geopos(key, ["Palermo", "Catania", "Place"]) + expected_positions = [ + [13.36138933897018433, 38.11555639549629859], + [15.08726745843887329, 37.50266842333162032], + ] + assert len(positions) == 3 and positions[2] is None + + assert all( + all( + math.isclose(actual_coord, expected_coord) + for actual_coord, expected_coord in zip(actual_pos, expected_pos) + ) + for actual_pos, expected_pos in zip(positions, expected_positions) + if actual_pos is not None + ) + + assert ( + await glide_client.geopos( + "non_existing_key", ["Palermo", "Catania", "Place"] + ) + == [None] * 3 + ) + + # Neccessary to check since we are enforcing the user to pass a list of members while valkey don't + # But when running the command with key only (and no members) the returned value will always be an empty list + # So in case of any changes, this test will fail and inform us that we should allow not passing any members. + assert await glide_client.geohash(key, []) == [] + + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.geopos(key, ["Palermo", "Catania"]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zadd_zaddincr(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + assert await glide_client.zadd_incr(key, member="one", increment=2) == 3.0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zadd_nx_xx(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert ( + await glide_client.zadd( + key, + members_scores=members_scores, + existing_options=ConditionalChange.ONLY_IF_EXISTS, + ) + == 0 + ) + assert ( + await glide_client.zadd( + key, + members_scores=members_scores, + existing_options=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, + ) + == 3 + ) + + assert ( + await glide_client.zadd_incr( + key, + member="one", + increment=5.0, + existing_options=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, + ) + is None + ) + + assert ( + await glide_client.zadd_incr( + key, + member="one", + increment=5.0, + existing_options=ConditionalChange.ONLY_IF_EXISTS, + ) + == 6.0 + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zadd_gt_lt(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Dict[TEncodable, float] = {"one": -3, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + members_scores["one"] = 10 + assert ( + await glide_client.zadd( + key, + members_scores=members_scores, + update_condition=UpdateOptions.GREATER_THAN, + changed=True, + ) + == 1 + ) + + assert ( + await glide_client.zadd( + key, + members_scores=members_scores, + update_condition=UpdateOptions.LESS_THAN, + changed=True, + ) + == 0 + ) + + assert ( + await glide_client.zadd_incr( + key, + member="one", + increment=-3.0, + update_condition=UpdateOptions.LESS_THAN, + ) + == 7.0 + ) + + assert ( + await glide_client.zadd_incr( + key, + member="one", + increment=-3.0, + update_condition=UpdateOptions.GREATER_THAN, + ) + is None + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zincrby(self, glide_client: TGlideClient): + key, member, member2 = ( + get_random_string(10), + get_random_string(5), + get_random_string(5), + ) + + # key does not exist + assert await glide_client.zincrby(key, 2.5, member) == 2.5 + assert await glide_client.zscore(key, member) == 2.5 + + # key exists, but value doesn't + assert await glide_client.zincrby(key, -3.3, member2) == -3.3 + assert await glide_client.zscore(key, member2) == -3.3 + + # updating existing value in existing key + assert await glide_client.zincrby(key, 1.0, member) == 3.5 + assert await glide_client.zscore(key, member) == 3.5 + + # Key exists, but it is not a sorted set + assert await glide_client.set(key, "_") == OK + with pytest.raises(RequestError): + await glide_client.zincrby(key, 0.5, "_") + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrem(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + + assert await glide_client.zrem(key, ["one"]) == 1 + assert await glide_client.zrem(key, ["one", "two", "three"]) == 2 + + assert await glide_client.zrem("non_existing_set", ["member"]) == 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zremrangebyscore(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores) == 3 + + assert ( + await glide_client.zremrangebyscore( + key, ScoreBoundary(1, False), ScoreBoundary(2) + ) + == 1 + ) + assert ( + await glide_client.zremrangebyscore(key, ScoreBoundary(1), InfBound.NEG_INF) + == 0 + ) + assert ( + await glide_client.zremrangebyscore( + "non_existing_set", InfBound.NEG_INF, InfBound.POS_INF + ) + == 0 + ) + + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zremrangebyscore(key, InfBound.NEG_INF, InfBound.POS_INF) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zremrangebylex(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + range = RangeByIndex(0, -1) + members_scores: Mapping[TEncodable, float] = {"a": 1, "b": 2, "c": 3, "d": 4} + assert await glide_client.zadd(key1, members_scores) == 4 + + assert ( + await glide_client.zremrangebylex( + key1, LexBoundary("a", False), LexBoundary("c") + ) + == 2 + ) + zremrangebylex_res = await glide_client.zrange_withscores(key1, range) + assert compare_maps(zremrangebylex_res, {"a": 1.0, "d": 4.0}) is True + + assert ( + await glide_client.zremrangebylex(key1, LexBoundary("d"), InfBound.POS_INF) + == 1 + ) + assert await glide_client.zrange_withscores(key1, range) == {b"a": 1.0} + + # min_lex > max_lex + assert ( + await glide_client.zremrangebylex(key1, LexBoundary("a"), InfBound.NEG_INF) + == 0 + ) + assert await glide_client.zrange_withscores(key1, range) == {b"a": 1.0} + + assert ( + await glide_client.zremrangebylex( + "non_existing_key", InfBound.NEG_INF, InfBound.POS_INF + ) + == 0 + ) + + # key exists, but it is not a sorted set + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.zremrangebylex( + key2, LexBoundary("a", False), LexBoundary("c") + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zremrangebyrank(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + range = RangeByIndex(0, -1) + members_scores: Mapping[TEncodable, float] = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + } + assert await glide_client.zadd(key1, members_scores) == 5 + + # Test start exceeding end + assert await glide_client.zremrangebyrank(key1, 2, 1) == 0 + + # Test removing elements by rank + assert await glide_client.zremrangebyrank(key1, 0, 2) == 3 + zremrangebyrank_res = await glide_client.zrange_withscores(key1, range) + assert compare_maps(zremrangebyrank_res, {"d": 4.0, "e": 5.0}) is True + + # Test removing elements beyond the existing range + assert await glide_client.zremrangebyrank(key1, 0, 10) == 2 + assert await glide_client.zrange_withscores(key1, range) == {} + + # Test with non-existing key + assert await glide_client.zremrangebyrank("non_existing_key", 0, 1) == 0 + + # Key exists, but it is not a sorted set + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.zremrangebyrank(key2, 0, 1) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zlexcount(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"a": 1.0, "b": 2.0, "c": 3.0} + + assert await glide_client.zadd(key1, members_scores) == 3 + assert ( + await glide_client.zlexcount(key1, InfBound.NEG_INF, InfBound.POS_INF) == 3 + ) + assert ( + await glide_client.zlexcount( + key1, + LexBoundary("a", is_inclusive=False), + LexBoundary("c", is_inclusive=True), + ) + == 2 + ) + assert ( + await glide_client.zlexcount( + key1, InfBound.NEG_INF, LexBoundary("c", is_inclusive=True) + ) + == 3 + ) + # Incorrect range; start > end + assert ( + await glide_client.zlexcount( + key1, InfBound.POS_INF, LexBoundary("c", is_inclusive=True) + ) + == 0 + ) + assert ( + await glide_client.zlexcount( + "non_existing_key", InfBound.NEG_INF, InfBound.POS_INF + ) + == 0 + ) + + # key exists, but it is not a sorted set + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.zlexcount(key2, InfBound.NEG_INF, InfBound.POS_INF) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zcard(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + assert await glide_client.zcard(key) == 3 + + assert await glide_client.zrem(key, ["one"]) == 1 + assert await glide_client.zcard(key) == 2 + assert await glide_client.zcard("non_existing_key") == 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zcount(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + + assert await glide_client.zcount(key, InfBound.NEG_INF, InfBound.POS_INF) == 3 + assert ( + await glide_client.zcount( + key, + ScoreBoundary(1, is_inclusive=False), + ScoreBoundary(3, is_inclusive=False), + ) + == 1 + ) + assert ( + await glide_client.zcount( + key, + ScoreBoundary(1, is_inclusive=False), + ScoreBoundary(3, is_inclusive=True), + ) + == 2 + ) + assert ( + await glide_client.zcount( + key, InfBound.NEG_INF, ScoreBoundary(3, is_inclusive=True) + ) + == 3 + ) + assert ( + await glide_client.zcount( + key, InfBound.POS_INF, ScoreBoundary(3, is_inclusive=True) + ) + == 0 + ) + assert ( + await glide_client.zcount( + "non_existing_key", InfBound.NEG_INF, InfBound.POS_INF + ) + == 0 + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zscore(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + assert await glide_client.zscore(key, "one") == 1.0 + + assert await glide_client.zscore(key, "non_existing_member") is None + assert ( + await glide_client.zscore("non_existing_key", "non_existing_member") is None + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zmscore(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + + assert await glide_client.zadd(key1, members_scores=members_scores) == 3 + assert await glide_client.zmscore(key1, ["one", "two", "three"]) == [ + 1.0, + 2.0, + 3.0, + ] + assert await glide_client.zmscore( + key1, ["one", "non_existing_member", "non_existing_member", "three"] + ) == [1.0, None, None, 3.0] + assert await glide_client.zmscore("non_existing_key", ["one"]) == [None] + + assert await glide_client.set(key2, "value") == OK + with pytest.raises(RequestError): + await glide_client.zmscore(key2, ["one"]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zinter_commands(self, glide_client: TGlideClient): + key1 = "{testKey}:1-" + get_random_string(10) + key2 = "{testKey}:2-" + get_random_string(10) + key3 = "{testKey}:3-" + get_random_string(10) + range = RangeByIndex(0, -1) + members_scores1: Mapping[TEncodable, float] = {"one": 1.0, "two": 2.0} + members_scores2: Mapping[TEncodable, float] = { + "one": 1.5, + "two": 2.5, + "three": 3.5, + } + + assert await glide_client.zadd(key1, members_scores1) == 2 + assert await glide_client.zadd(key2, members_scores2) == 3 + + # zinter tests + zinter_map = await glide_client.zinter([key1, key2]) + expected_zinter_map = [b"one", b"two"] + assert zinter_map == expected_zinter_map + + # zinterstore tests + assert await glide_client.zinterstore(key3, [key1, key2]) == 2 + zinterstore_map = await glide_client.zrange_withscores(key3, range) + expected_zinter_map_withscores = { + b"one": 2.5, + b"two": 4.5, + } + assert compare_maps(zinterstore_map, expected_zinter_map_withscores) is True + + # zinter_withscores tests + zinter_withscores_map = await glide_client.zinter_withscores([key1, key2]) + assert ( + compare_maps(zinter_withscores_map, expected_zinter_map_withscores) is True + ) + + # MAX aggregation tests + assert ( + await glide_client.zinterstore(key3, [key1, key2], AggregationType.MAX) == 2 + ) + zinterstore_map_max = await glide_client.zrange_withscores(key3, range) + expected_zinter_map_max = { + b"one": 1.5, + b"two": 2.5, + } + assert compare_maps(zinterstore_map_max, expected_zinter_map_max) is True + + zinter_withscores_map_max = await glide_client.zinter_withscores( + [key1, key2], AggregationType.MAX + ) + assert compare_maps(zinter_withscores_map_max, expected_zinter_map_max) is True + + # MIN aggregation tests + assert ( + await glide_client.zinterstore(key3, [key1, key2], AggregationType.MIN) == 2 + ) + zinterstore_map_min = await glide_client.zrange_withscores(key3, range) + expected_zinter_map_min = { + b"one": 1.0, + b"two": 2.0, + } + assert compare_maps(zinterstore_map_min, expected_zinter_map_min) is True + + zinter_withscores_map_min = await glide_client.zinter_withscores( + [key1, key2], AggregationType.MIN + ) + assert compare_maps(zinter_withscores_map_min, expected_zinter_map_min) is True + + # SUM aggregation tests + assert ( + await glide_client.zinterstore(key3, [key1, key2], AggregationType.SUM) == 2 + ) + zinterstore_map_sum = await glide_client.zrange_withscores(key3, range) + assert compare_maps(zinterstore_map_sum, expected_zinter_map_withscores) is True + + zinter_withscores_map_sum = await glide_client.zinter_withscores( + [key1, key2], AggregationType.SUM + ) + assert ( + compare_maps(zinter_withscores_map_sum, expected_zinter_map_withscores) + is True + ) + + # Multiplying scores during aggregation tests + assert ( + await glide_client.zinterstore( + key3, [(key1, 2.0), (key2, 2.0)], AggregationType.SUM + ) + == 2 + ) + zinterstore_map_multiplied = await glide_client.zrange_withscores(key3, range) + expected_zinter_map_multiplied = { + b"one": 5.0, + b"two": 9.0, + } + assert ( + compare_maps(zinterstore_map_multiplied, expected_zinter_map_multiplied) + is True + ) + + zinter_withscores_map_multiplied = await glide_client.zinter_withscores( + [(key1, 2.0), (key2, 2.0)], AggregationType.SUM + ) + assert ( + compare_maps( + zinter_withscores_map_multiplied, expected_zinter_map_multiplied + ) + is True + ) + + # Non-existing key test + assert ( + await glide_client.zinterstore(key3, [key1, "{testKey}-non_existing_key"]) + == 0 + ) + zinter_withscores_non_existing = await glide_client.zinter_withscores( + [key1, "{testKey}-non_existing_key"] + ) + assert zinter_withscores_non_existing == {} + + # Empty list check + with pytest.raises(RequestError) as e: + await glide_client.zinterstore( + "{xyz}", cast(List[TEncodable], cast(List[TEncodable], [])) + ) + assert "wrong number of arguments" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.zinter([]) + assert "wrong number of arguments" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.zinter_withscores(cast(List[TEncodable], [])) + assert "at least 1 input key is needed" in str(e) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zunion_commands(self, glide_client: TGlideClient): + key1 = "{testKey}:1-" + get_random_string(10) + key2 = "{testKey}:2-" + get_random_string(10) + key3 = "{testKey}:3-" + get_random_string(10) + range = RangeByIndex(0, -1) + members_scores1: Mapping[TEncodable, float] = {"one": 1.0, "two": 2.0} + members_scores2: Mapping[TEncodable, float] = { + "one": 1.5, + "two": 2.5, + "three": 3.5, + } + + assert await glide_client.zadd(key1, members_scores1) == 2 + assert await glide_client.zadd(key2, members_scores2) == 3 + + # zunion tests + zunion_map = await glide_client.zunion([key1, key2]) + expected_zunion_map = [b"one", b"three", b"two"] + assert zunion_map == expected_zunion_map + + # zunionstore tests + assert await glide_client.zunionstore(key3, [key1, key2]) == 3 + zunionstore_map = await glide_client.zrange_withscores(key3, range) + expected_zunion_map_withscores = { + b"one": 2.5, + b"three": 3.5, + b"two": 4.5, + } + assert compare_maps(zunionstore_map, expected_zunion_map_withscores) is True + + # zunion_withscores tests + zunion_withscores_map = await glide_client.zunion_withscores([key1, key2]) + assert ( + compare_maps(zunion_withscores_map, expected_zunion_map_withscores) is True + ) + + # MAX aggregation tests + assert ( + await glide_client.zunionstore(key3, [key1, key2], AggregationType.MAX) == 3 + ) + zunionstore_map_max = await glide_client.zrange_withscores(key3, range) + expected_zunion_map_max = { + b"one": 1.5, + b"two": 2.5, + b"three": 3.5, + } + assert compare_maps(zunionstore_map_max, expected_zunion_map_max) is True + + zunion_withscores_map_max = await glide_client.zunion_withscores( + [key1, key2], AggregationType.MAX + ) + assert compare_maps(zunion_withscores_map_max, expected_zunion_map_max) is True + + # MIN aggregation tests + assert ( + await glide_client.zunionstore(key3, [key1, key2], AggregationType.MIN) == 3 + ) + zunionstore_map_min = await glide_client.zrange_withscores(key3, range) + expected_zunion_map_min = { + b"one": 1.0, + b"two": 2.0, + b"three": 3.5, + } + assert compare_maps(zunionstore_map_min, expected_zunion_map_min) is True + + zunion_withscores_map_min = await glide_client.zunion_withscores( + [key1, key2], AggregationType.MIN + ) + assert compare_maps(zunion_withscores_map_min, expected_zunion_map_min) is True + + # SUM aggregation tests + assert ( + await glide_client.zunionstore(key3, [key1, key2], AggregationType.SUM) == 3 + ) + zunionstore_map_sum = await glide_client.zrange_withscores(key3, range) + assert compare_maps(zunionstore_map_sum, expected_zunion_map_withscores) is True + + zunion_withscores_map_sum = await glide_client.zunion_withscores( + [key1, key2], AggregationType.SUM + ) + assert ( + compare_maps(zunion_withscores_map_sum, expected_zunion_map_withscores) + is True + ) + + # Multiplying scores during aggregation tests + assert ( + await glide_client.zunionstore( + key3, [(key1, 2.0), (key2, 2.0)], AggregationType.SUM + ) + == 3 + ) + zunionstore_map_multiplied = await glide_client.zrange_withscores(key3, range) + expected_zunion_map_multiplied = { + b"one": 5.0, + b"three": 7.0, + b"two": 9.0, + } + assert ( + compare_maps(zunionstore_map_multiplied, expected_zunion_map_multiplied) + is True + ) + + zunion_withscores_map_multiplied = await glide_client.zunion_withscores( + [(key1, 2.0), (key2, 2.0)], AggregationType.SUM + ) + assert ( + compare_maps( + zunion_withscores_map_multiplied, expected_zunion_map_multiplied + ) + is True + ) + + # Non-existing key test + assert ( + await glide_client.zunionstore(key3, [key1, "{testKey}-non_existing_key"]) + == 2 + ) + zunionstore_map_nonexistingkey = await glide_client.zrange_withscores( + key3, range + ) + expected_zunion_map_nonexistingkey = { + b"one": 1.0, + b"two": 2.0, + } + assert ( + compare_maps( + zunionstore_map_nonexistingkey, expected_zunion_map_nonexistingkey + ) + is True + ) + + zunion_withscores_non_existing = await glide_client.zunion_withscores( + [key1, "{testKey}-non_existing_key"] + ) + assert ( + compare_maps( + zunion_withscores_non_existing, expected_zunion_map_nonexistingkey + ) + is True + ) + + # Empty list check + with pytest.raises(RequestError) as e: + await glide_client.zunionstore("{xyz}", cast(List[TEncodable], [])) + assert "wrong number of arguments" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.zunion([]) + assert "wrong number of arguments" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.zunion_withscores(cast(List[TEncodable], [])) + assert "at least 1 input key is needed" in str(e) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zpopmin(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"a": 1.0, "b": 2.0, "c": 3.0} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + assert await glide_client.zpopmin(key) == {b"a": 1.0} + + zpopmin_map = await glide_client.zpopmin(key, 3) + expected_map = {b"b": 2.0, b"c": 3.0} + assert compare_maps(zpopmin_map, expected_map) is True + + assert await glide_client.zpopmin(key) == {} + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zpopmin(key) + + assert await glide_client.zpopmin("non_exisitng_key") == {} + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bzpopmin(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + + assert await glide_client.zadd(key1, {"a": 1.0, "b": 1.5}) == 2 + assert await glide_client.zadd(key2, {"c": 2.0}) == 1 + assert await glide_client.bzpopmin( + [key1, key2], 0.5 + ) == convert_string_to_bytes_object([key1, "a", 1.0]) + assert await glide_client.bzpopmin( + [non_existing_key, key2], 0.5 + ) == convert_string_to_bytes_object( + [ + key2, + "c", + 2.0, + ] + ) + assert await glide_client.bzpopmin(["non_existing_key"], 0.5) is None + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.bzpopmin([], 0.5) + + # key exists, but it is not a sorted set + assert await glide_client.set("foo", "value") == OK + with pytest.raises(RequestError): + await glide_client.bzpopmin(["foo"], 0.5) + + async def endless_bzpopmin_call(): + await glide_client.bzpopmin(["non_existent_key"], 0) + + # bzpopmin is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_bzpopmin_call(), timeout=0.5) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zpopmax(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"a": 1.0, "b": 2.0, "c": 3.0} + assert await glide_client.zadd(key, members_scores) == 3 + assert await glide_client.zpopmax(key) == {b"c": 3.0} + + zpopmax_map = await glide_client.zpopmax(key, 3) + expected_map = {"b": 2.0, "a": 1.0} + assert compare_maps(zpopmax_map, expected_map) is True + + assert await glide_client.zpopmax(key) == {} + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zpopmax(key) + + assert await glide_client.zpopmax("non_exisitng_key") == {} + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bzpopmax(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + + assert await glide_client.zadd(key1, {"a": 1.0, "b": 1.5}) == 2 + assert await glide_client.zadd(key2, {"c": 2.0}) == 1 + assert await glide_client.bzpopmax( + [key1, key2], 0.5 + ) == convert_string_to_bytes_object([key1, "b", 1.5]) + assert await glide_client.bzpopmax( + [non_existing_key, key2], 0.5 + ) == convert_string_to_bytes_object( + [ + key2, + "c", + 2.0, + ] + ) + assert await glide_client.bzpopmax(["non_existing_key"], 0.5) is None + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.bzpopmax([], 0.5) + + # key exists, but it is not a sorted set + assert await glide_client.set("foo", "value") == OK + with pytest.raises(RequestError): + await glide_client.bzpopmax(["foo"], 0.5) + + async def endless_bzpopmax_call(): + await glide_client.bzpopmax(["non_existent_key"], 0) + + # bzpopmax is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_bzpopmax_call(), timeout=0.5) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_by_index(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + + assert await glide_client.zrange(key, RangeByIndex(start=0, stop=1)) == [ + b"one", + b"two", + ] + + zrange_map = await glide_client.zrange_withscores( + key, RangeByIndex(start=0, stop=-1) + ) + expected_map = {b"one": 1.0, b"two": 2.0, b"three": 3.0} + assert compare_maps(zrange_map, expected_map) is True + + assert await glide_client.zrange( + key, RangeByIndex(start=0, stop=1), reverse=True + ) == [ + b"three", + b"two", + ] + + assert await glide_client.zrange(key, RangeByIndex(start=3, stop=1)) == [] + assert ( + await glide_client.zrange_withscores(key, RangeByIndex(start=3, stop=1)) + == {} + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_byscore(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + + assert await glide_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) == [b"one", b"two"] + + zrange_map = await glide_client.zrange_withscores( + key, + RangeByScore(start=InfBound.NEG_INF, stop=InfBound.POS_INF), + ) + expected_map = {b"one": 1.0, b"two": 2.0, b"three": 3.0} + assert compare_maps(zrange_map, expected_map) is True + + assert await glide_client.zrange( + key, + RangeByScore( + start=ScoreBoundary(3, is_inclusive=False), stop=InfBound.NEG_INF + ), + reverse=True, + ) == [b"two", b"one"] + + assert ( + await glide_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, + stop=InfBound.POS_INF, + limit=Limit(offset=1, count=2), + ), + ) + ) == [b"two", b"three"] + + assert ( + await glide_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + reverse=True, + ) + == [] + ) # stop is greater than start with reverse set to True + + assert ( + await glide_client.zrange( + key, + RangeByScore( + start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) + == [] + ) # start is greater than stop + + assert ( + await glide_client.zrange_withscores( + key, + RangeByScore( + start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) + == {} + ) # start is greater than stop + + assert ( + await glide_client.zrange_withscores( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + reverse=True, + ) + == {} + ) # stop is greater than start with reverse set to True + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_bylex(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"a": 1, "b": 2, "c": 3} + assert await glide_client.zadd(key, members_scores=members_scores) == 3 + + assert await glide_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) + ), + ) == [b"a", b"b"] + + assert ( + await glide_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, + stop=InfBound.POS_INF, + limit=Limit(offset=1, count=2), + ), + ) + ) == [b"b", b"c"] + + assert await glide_client.zrange( + key, + RangeByLex( + start=LexBoundary("c", is_inclusive=False), stop=InfBound.NEG_INF + ), + reverse=True, + ) == [b"b", b"a"] + + assert ( + await glide_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) + ), + reverse=True, + ) + == [] + ) # stop is greater than start with reverse set to True + + assert ( + await glide_client.zrange( + key, + RangeByLex( + start=InfBound.POS_INF, stop=LexBoundary("c", is_inclusive=False) + ), + ) + == [] + ) # start is greater than stop + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_different_types_of_keys(self, glide_client: TGlideClient): + key = get_random_string(10) + + assert ( + await glide_client.zrange("non_existing_key", RangeByIndex(start=0, stop=1)) + == [] + ) + + assert ( + await glide_client.zrange_withscores( + "non_existing_key", RangeByIndex(start=0, stop=-1) + ) + ) == {} + + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrange(key, RangeByIndex(start=0, stop=1)) + + with pytest.raises(RequestError): + await glide_client.zrange_withscores(key, RangeByIndex(start=0, stop=1)) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrangestore_by_index(self, glide_client: TGlideClient): + destination = f"{{testKey}}:{get_random_string(10)}" + source = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:{get_random_string(10)}" + + member_scores: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + assert await glide_client.zadd(source, member_scores) == 3 + + # full range + assert ( + await glide_client.zrangestore(destination, source, RangeByIndex(0, -1)) + == 3 + ) + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"one": 1.0, "two": 2.0, "three": 3.0}) is True + + # range from rank 0 to 1, from highest to lowest score + assert ( + await glide_client.zrangestore( + destination, source, RangeByIndex(0, 1), True + ) + == 2 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"two": 2.0, "three": 3.0}) is True + + # incorrect range, as start > stop + assert ( + await glide_client.zrangestore(destination, source, RangeByIndex(3, 1)) == 0 + ) + assert ( + await glide_client.zrange_withscores(destination, RangeByIndex(0, -1)) == {} + ) + + # non-existing source + assert ( + await glide_client.zrangestore( + destination, non_existing_key, RangeByIndex(0, -1) + ) + == 0 + ) + assert ( + await glide_client.zrange_withscores(destination, RangeByIndex(0, -1)) == {} + ) + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrangestore(destination, string_key, RangeByIndex(0, -1)) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrangestore_by_score(self, glide_client: TGlideClient): + destination = f"{{testKey}}:{get_random_string(10)}" + source = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:{get_random_string(10)}" + + member_scores: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + assert await glide_client.zadd(source, member_scores) == 3 + + # range from negative infinity to 3 (exclusive) + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByScore(InfBound.NEG_INF, ScoreBoundary(3, False)), + ) + == 2 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"one": 1.0, "two": 2.0}) is True + + # range from 1 (inclusive) to positive infinity + assert ( + await glide_client.zrangestore( + destination, source, RangeByScore(ScoreBoundary(1), InfBound.POS_INF) + ) + == 3 + ) + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"one": 1.0, "two": 2.0, "three": 3.0}) is True + + # range from negative to positive infinity, limited to ranks 1 to 2 + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByScore(InfBound.NEG_INF, InfBound.POS_INF, Limit(1, 2)), + ) + == 2 + ) + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"two": 2.0, "three": 3.0}) is True + + # range from positive to negative infinity reversed, limited to ranks 1 to 2 + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByScore(InfBound.POS_INF, InfBound.NEG_INF, Limit(1, 2)), + True, + ) + == 2 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"one": 1.0, "two": 2.0}) is True + + # incorrect range as start > stop + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByScore(ScoreBoundary(3, False), InfBound.NEG_INF), + ) + == 0 + ) + assert ( + await glide_client.zrange_withscores(destination, RangeByIndex(0, -1)) == {} + ) + + # non-existing source + assert ( + await glide_client.zrangestore( + destination, + non_existing_key, + RangeByScore(InfBound.NEG_INF, ScoreBoundary(3, False)), + ) + == 0 + ) + assert ( + await glide_client.zrange_withscores(destination, RangeByIndex(0, -1)) == {} + ) + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrangestore( + destination, + string_key, + RangeByScore(ScoreBoundary(0), ScoreBoundary(3)), + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrangestore_by_lex(self, glide_client: TGlideClient): + destination = f"{{testKey}}:{get_random_string(10)}" + source = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + member_scores: Mapping[TEncodable, float] = {"a": 1.0, "b": 2.0, "c": 3.0} + assert await glide_client.zadd(source, member_scores) == 3 + + # range from negative infinity to "c" (exclusive) + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByLex(InfBound.NEG_INF, LexBoundary("c", False)), + ) + == 2 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"a": 1.0, "b": 2.0}) is True + + # range from "a" (inclusive) to positive infinity + assert ( + await glide_client.zrangestore( + destination, source, RangeByLex(LexBoundary("a"), InfBound.POS_INF) + ) + == 3 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"a": 1.0, "b": 2.0, "c": 3.0}) is True + + # range from negative to positive infinity, limited to ranks 1 to 2 + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByLex(InfBound.NEG_INF, InfBound.POS_INF, Limit(1, 2)), + ) + == 2 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"b": 2.0, "c": 3.0}) is True + + # range from positive to negative infinity reversed, limited to ranks 1 to 2 + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByLex(InfBound.POS_INF, InfBound.NEG_INF, Limit(1, 2)), + True, + ) + == 2 + ) + + zrange_res = await glide_client.zrange_withscores( + destination, RangeByIndex(0, -1) + ) + assert compare_maps(zrange_res, {"a": 1.0, "b": 2.0}) is True + + # incorrect range as start > stop + assert ( + await glide_client.zrangestore( + destination, + source, + RangeByLex(LexBoundary("c", False), InfBound.NEG_INF), + ) + == 0 + ) + assert ( + await glide_client.zrange_withscores(destination, RangeByIndex(0, -1)) == {} + ) + + # non-existing source + assert ( + await glide_client.zrangestore( + destination, + non_existing_key, + RangeByLex(InfBound.NEG_INF, InfBound.POS_INF), + ) + == 0 + ) + assert ( + await glide_client.zrange_withscores(destination, RangeByIndex(0, -1)) == {} + ) + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrangestore( + destination, string_key, RangeByLex(InfBound.NEG_INF, InfBound.POS_INF) + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrank(self, glide_client: TGlideClient): + key = get_random_string(10) + members_scores: Mapping[TEncodable, float] = {"one": 1.5, "two": 2, "three": 3} + assert await glide_client.zadd(key, members_scores) == 3 + assert await glide_client.zrank(key, "one") == 0 + if not await check_if_server_version_lt(glide_client, "7.2.0"): + assert await glide_client.zrank_withscore(key, "one") == [0, 1.5] + assert await glide_client.zrank_withscore(key, "non_existing_field") is None + assert ( + await glide_client.zrank_withscore("non_existing_key", "field") is None + ) + + assert await glide_client.zrank(key, "non_existing_field") is None + assert await glide_client.zrank("non_existing_key", "field") is None + + assert await glide_client.set(key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrank(key, "one") + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrevrank(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + string_key = get_random_string(10) + member_scores: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + + assert await glide_client.zadd(key, member_scores) == 3 + assert await glide_client.zrevrank(key, "three") == 0 + assert await glide_client.zrevrank(key, "non_existing_member") is None + assert ( + await glide_client.zrevrank(non_existing_key, "non_existing_member") is None + ) + + if not await check_if_server_version_lt(glide_client, "7.2.0"): + assert await glide_client.zrevrank_withscore(key, "one") == [2, 1.0] + assert ( + await glide_client.zrevrank_withscore(key, "non_existing_member") + is None + ) + assert ( + await glide_client.zrevrank_withscore( + non_existing_key, "non_existing_member" + ) + is None + ) + + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.zrevrank(string_key, "member") + with pytest.raises(RequestError): + await glide_client.zrevrank_withscore(string_key, "member") + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zdiff(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + member_scores1: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + member_scores2: Mapping[TEncodable, float] = {"two": 2.0} + member_scores3: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + "four": 4.0, + } + + assert await glide_client.zadd(key1, member_scores1) == 3 + assert await glide_client.zadd(key2, member_scores2) == 1 + assert await glide_client.zadd(key3, member_scores3) == 4 + + assert await glide_client.zdiff([key1, key2]) == [b"one", b"three"] + assert await glide_client.zdiff([key1, key3]) == [] + assert await glide_client.zdiff([non_existing_key, key3]) == [] + + zdiff_map = await glide_client.zdiff_withscores([key1, key2]) + expected_map = { + b"one": 1.0, + b"three": 3.0, + } + assert compare_maps(zdiff_map, expected_map) is True + assert ( + compare_maps(await glide_client.zdiff_withscores([key1, key3]), {}) is True # type: ignore + ) + non_exist_res = await glide_client.zdiff_withscores([non_existing_key, key3]) + assert non_exist_res == {} + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.zdiff([]) + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.zdiff_withscores([]) + + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.zdiff([string_key, key2]) + + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.zdiff_withscores([string_key, key2]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zdiffstore(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + key4 = f"{{testKey}}:4-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + member_scores1: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + member_scores2: Mapping[TEncodable, float] = {"two": 2.0} + member_scores3: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + "four": 4.0, + } + + assert await glide_client.zadd(key1, member_scores1) == 3 + assert await glide_client.zadd(key2, member_scores2) == 1 + assert await glide_client.zadd(key3, member_scores3) == 4 + + assert await glide_client.zdiffstore(key4, [key1, key2]) == 2 + + zrange_res = await glide_client.zrange_withscores(key4, RangeByIndex(0, -1)) + assert compare_maps(zrange_res, {"one": 1.0, "three": 3.0}) is True + + assert await glide_client.zdiffstore(key4, [key3, key2, key1]) == 1 + assert await glide_client.zrange_withscores(key4, RangeByIndex(0, -1)) == { + b"four": 4.0 + } + + assert await glide_client.zdiffstore(key4, [key1, key3]) == 0 + assert await glide_client.zrange_withscores(key4, RangeByIndex(0, -1)) == {} + + assert await glide_client.zdiffstore(key4, [non_existing_key, key1]) == 0 + assert await glide_client.zrange_withscores(key4, RangeByIndex(0, -1)) == {} + + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zdiffstore(key4, [string_key, key1]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bzmpop(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{test}}-1-f{get_random_string(10)}" + key2 = f"{{test}}-2-f{get_random_string(10)}" + non_existing_key = f"{{test}}-non_existing_key" + string_key = f"{{test}}-3-f{get_random_string(10)}" + + assert ( + await glide_client.zadd( + key1, cast(Mapping[TEncodable, float], {"a1": 1, "b1": 2}) + ) + == 2 + ) + assert ( + await glide_client.zadd( + key2, cast(Mapping[TEncodable, float], {"a2": 0.1, "b2": 0.2}) + ) + == 2 + ) + + assert await glide_client.bzmpop([key1, key2], ScoreFilter.MAX, 0.1) == [ + key1.encode(), + {b"b1": 2}, + ] + assert await glide_client.bzmpop([key2, key1], ScoreFilter.MAX, 0.1, 10) == [ + key2.encode(), + {b"b2": 0.2, b"a2": 0.1}, + ] + + # ensure that command doesn't time out even if timeout > request timeout (250ms by default) + assert ( + await glide_client.bzmpop([non_existing_key], ScoreFilter.MIN, 0.5) is None + ) + assert ( + await glide_client.bzmpop([non_existing_key], ScoreFilter.MIN, 0.55, 1) + is None + ) + + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.bzmpop([string_key], ScoreFilter.MAX, 0.1) + with pytest.raises(RequestError): + await glide_client.bzmpop([string_key], ScoreFilter.MAX, 0.1, 1) + + # incorrect argument: key list should not be empty + with pytest.raises(RequestError): + assert await glide_client.bzmpop([], ScoreFilter.MAX, 0.1, 1) + + # incorrect argument: count should be greater than 0 + with pytest.raises(RequestError): + assert await glide_client.bzmpop([key1], ScoreFilter.MAX, 0.1, 0) + + # check that order of entries in the response is preserved + entries: Dict[TEncodable, float] = {} + for i in range(0, 10): + entries.update({f"a{i}": float(i)}) + + assert await glide_client.zadd(key2, entries) == 10 + result = await glide_client.bzmpop([key2], ScoreFilter.MIN, 0.1, 10) + assert result is not None + result_map = cast(Mapping[bytes, float], result[1]) + assert compare_maps(entries, result_map) is True # type: ignore + + async def endless_bzmpop_call(): + await glide_client.bzmpop(["non_existent_key"], ScoreFilter.MAX, 0) + + # bzmpop is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_bzmpop_call(), timeout=0.5) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrandmember(self, glide_client: TGlideClient): + key = get_random_string(10) + string_key = get_random_string(10) + scores: Mapping[TEncodable, float] = {"one": 1, "two": 2} + assert await glide_client.zadd(key, scores) == 2 + + member = await glide_client.zrandmember(key) + # TODO: remove when functions API is fixed + assert isinstance(member, bytes) + assert member.decode() in scores + assert await glide_client.zrandmember("non_existing_key") is None + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrandmember(string_key) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrandmember_count(self, glide_client: TGlideClient): + key = get_random_string(10) + string_key = get_random_string(10) + scores: Mapping[TEncodable, float] = {"one": 1, "two": 2} + assert await glide_client.zadd(key, scores) == 2 + + # unique values are expected as count is positive + members = await glide_client.zrandmember_count(key, 4) + assert len(members) == 2 + assert set(members) == {b"one", b"two"} + + # duplicate values are expected as count is negative + members = await glide_client.zrandmember_count(key, -4) + assert len(members) == 4 + for member in members: + # TODO: remove when functions API is fixed + assert isinstance(member, bytes) + assert member.decode() in scores + + assert await glide_client.zrandmember_count(key, 0) == [] + assert await glide_client.zrandmember_count("non_existing_key", 0) == [] + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrandmember_count(string_key, 5) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrandmember_withscores(self, glide_client: TGlideClient): + key = get_random_string(10) + string_key = get_random_string(10) + scores: Mapping[TEncodable, float] = {"one": 1, "two": 2} + assert await glide_client.zadd(key, scores) == 2 + + # unique values are expected as count is positive + elements = await glide_client.zrandmember_withscores(key, 4) + assert len(elements) == 2 + + for member, score in elements: + # TODO: remove when functions API is fixed + assert isinstance(member, bytes) + assert scores[(member).decode()] == score + + # duplicate values are expected as count is negative + elements = await glide_client.zrandmember_withscores(key, -4) + assert len(elements) == 4 + for member, score in elements: + # TODO: remove when functions API is fixed + assert isinstance(member, bytes) + assert scores[(member).decode()] == score + + assert await glide_client.zrandmember_withscores(key, 0) == [] + assert await glide_client.zrandmember_withscores("non_existing_key", 0) == [] + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zrandmember_withscores(string_key, 5) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zintercard(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + member_scores1: Mapping[TEncodable, float] = { + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + member_scores2: Mapping[TEncodable, float] = { + "two": 2.0, + "three": 3.0, + "four": 4.0, + } + + assert await glide_client.zadd(key1, member_scores1) == 3 + assert await glide_client.zadd(key2, member_scores2) == 3 + + assert await glide_client.zintercard([key1, key2]) == 2 + assert await glide_client.zintercard([key1, non_existing_key]) == 0 + + assert await glide_client.zintercard([key1, key2], 0) == 2 + assert await glide_client.zintercard([key1, key2], 1) == 1 + assert await glide_client.zintercard([key1, key2], 3) == 2 + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await glide_client.zintercard([]) + + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zintercard([string_key]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zmpop(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{test}}-1-f{get_random_string(10)}" + key2 = f"{{test}}-2-f{get_random_string(10)}" + non_existing_key = f"{{test}}-non_existing_key" + string_key = f"{{test}}-3-f{get_random_string(10)}" + + assert await glide_client.zadd(key1, {"a1": 1, "b1": 2}) == 2 + assert await glide_client.zadd(key2, {"a2": 0.1, "b2": 0.2}) == 2 + + assert await glide_client.zmpop([key1, key2], ScoreFilter.MAX) == [ + key1.encode(), + {b"b1": 2}, + ] + assert await glide_client.zmpop([key2, key1], ScoreFilter.MAX, 10) == [ + key2.encode(), + {b"b2": 0.2, b"a2": 0.1}, + ] + + assert await glide_client.zmpop([non_existing_key], ScoreFilter.MIN) is None + assert await glide_client.zmpop([non_existing_key], ScoreFilter.MIN, 1) is None + + # key exists, but it is not a sorted set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.zmpop([string_key], ScoreFilter.MAX) + with pytest.raises(RequestError): + await glide_client.zmpop([string_key], ScoreFilter.MAX, 1) + + # incorrect argument: key list should not be empty + with pytest.raises(RequestError): + assert await glide_client.zmpop([], ScoreFilter.MAX, 1) + + # incorrect argument: count should be greater than 0 + with pytest.raises(RequestError): + assert await glide_client.zmpop([key1], ScoreFilter.MAX, 0) + + # check that order of entries in the response is preserved + entries: Dict[TEncodable, float] = {} + for i in range(0, 10): + entries[f"a{i}"] = float(i) + + assert await glide_client.zadd(key2, entries) == 10 + result = await glide_client.zmpop([key2], ScoreFilter.MIN, 10) + assert result is not None + result_map = cast(Mapping[bytes, float], result[1]) + assert compare_maps(entries, result_map) is True # type: ignore + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_type(self, glide_client: TGlideClient): + key = get_random_string(10) + assert await glide_client.set(key, "value") == OK + assert (await glide_client.type(key)).lower() == b"string" + assert await glide_client.delete([key]) == 1 + + assert await glide_client.set(key.encode(), "value") == OK + assert (await glide_client.type(key.encode())).lower() == b"string" + assert await glide_client.delete([key.encode()]) == 1 + + assert await glide_client.lpush(key, ["value"]) == 1 + assert (await glide_client.type(key)).lower() == b"list" + assert await glide_client.delete([key]) == 1 + + assert await glide_client.sadd(key, ["value"]) == 1 + assert (await glide_client.type(key)).lower() == b"set" + assert await glide_client.delete([key]) == 1 + + assert await glide_client.zadd(key, {"member": 1.0}) == 1 + assert (await glide_client.type(key)).lower() == b"zset" + assert await glide_client.delete([key]) == 1 + + assert await glide_client.hset(key, {"field": "value"}) == 1 + assert (await glide_client.type(key)).lower() == b"hash" + assert await glide_client.delete([key]) == 1 + + await glide_client.xadd(key, [("field", "value")]) + assert await glide_client.type(key) == b"stream" + assert await glide_client.delete([key]) == 1 + + assert (await glide_client.type(key)).lower() == b"none" + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sort_and_sort_store_with_get_or_by_args( + self, glide_client: GlideClient + ): + key = "{SameSlotKey}" + get_random_string(10) + store = "{SameSlotKey}" + get_random_string(10) + user_key1, user_key2, user_key3, user_key4, user_key5 = ( + "user:1", + "user:2", + "user:3", + "user:4", + "user:5", + ) + + # Prepare some data. Some keys and values randomaly encoded + assert await glide_client.hset(user_key1, {"name": "Alice", "age": "30"}) == 2 + assert ( + await glide_client.hset(user_key2.encode(), {"name": "Bob", "age": "25"}) + == 2 + ) + assert await glide_client.hset(user_key3, {"name": "Charlie", "age": "35"}) == 2 + assert ( + await glide_client.hset(user_key4, {"name": "Dave", "age".encode(): "20"}) + == 2 + ) + assert ( + await glide_client.hset(user_key5, {"name": "Eve", "age": "40".encode()}) + == 2 + ) + assert await glide_client.lpush("user_ids", ["5", "4", "3", "2", "1"]) == 5 + + # SORT_RO Available since: 7.0.0 + skip_sort_ro_test = False + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + skip_sort_ro_test = True + + # Test sort with all arguments + assert await glide_client.lpush(key, ["3", "1", "2"]) == 3 + result = await glide_client.sort( + key, + limit=Limit(0, 2), + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + assert result == [b"Alice", b"Bob"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro( + key, + limit=Limit(0, 2), + get_patterns=[b"user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + assert result_ro == [b"Alice", b"Bob"] + + # Test sort_store with all arguments + sort_store_result = await glide_client.sort_store( + key, + store, + limit=Limit(0, 2), + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + assert sort_store_result == 2 + sorted_list = await glide_client.lrange(store, 0, -1) + assert sorted_list == [b"Alice", b"Bob"] + + # Test sort with `by` argument + result = await glide_client.sort( + "user_ids", + by_pattern="user:*->age", + get_patterns=["user:*->name"], + alpha=True, + ) + assert result == [b"Dave", b"Bob", b"Alice", b"Charlie", b"Eve"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro( + b"user_ids", + by_pattern=b"user:*->age", + get_patterns=["user:*->name"], + alpha=True, + ) + assert result_ro == [b"Dave", b"Bob", b"Alice", b"Charlie", b"Eve"] + + # Test sort with `by` argument with missing keys to sort by + assert await glide_client.lpush("user_ids", ["a"]) == 6 + result = await glide_client.sort( + "user_ids", + by_pattern="user:*->age", + get_patterns=["user:*->name"], + alpha=True, + ) + assert result == convert_string_to_bytes_object( + [None, "Dave", "Bob", "Alice", "Charlie", "Eve"] + ) + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro( + "user_ids", + by_pattern=b"user:*->age", + get_patterns=["user:*->name"], + alpha=True, + ) + assert result_ro == [None, b"Dave", b"Bob", b"Alice", b"Charlie", b"Eve"] + + # Test sort with `by` argument with missing keys to sort by + result = await glide_client.sort( + "user_ids", + by_pattern="user:*->name", + get_patterns=["user:*->age"], + alpha=True, + ) + assert result == convert_string_to_bytes_object( + [None, "30", "25", "35", "20", "40"] + ) + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro( + "user_ids", + by_pattern=b"user:*->name", + get_patterns=[b"user:*->age"], + alpha=True, + ) + assert result_ro == [None, b"30", b"25", b"35", b"20", b"40"] + + # Test Limit with count 0 + result = await glide_client.sort( + "user_ids", + limit=Limit(0, 0), + alpha=True, + ) + assert result == [] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro( + "user_ids", + limit=Limit(0, 0), + alpha=True, + ) + assert result_ro == [] + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sort_and_sort_store_without_get_or_by_args( + self, glide_client: TGlideClient + ): + key = "{SameSlotKey}" + get_random_string(10) + store = "{SameSlotKey}" + get_random_string(10) + + # SORT_RO Available since: 7.0.0 + skip_sort_ro_test = False + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + skip_sort_ro_test = True + + # Test sort with non-existing key + result = await glide_client.sort("non_existing_key") + assert result == [] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro(b"non_existing_key") + assert result_ro == [] + + # Test sort_store with non-existing key + sort_store_result = await glide_client.sort_store( + "{SameSlotKey}:non_existing_key", store + ) + assert sort_store_result == 0 + + # Test each argument separately + assert await glide_client.lpush(key, ["5", "2", "4", "1", "3"]) == 5 + + # Test w/o flags + result = await glide_client.sort(key) + assert result == [b"1", b"2", b"3", b"4", b"5"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro(key) + assert result_ro == [b"1", b"2", b"3", b"4", b"5"] + + # limit argument + result = await glide_client.sort(key, limit=Limit(1, 3)) + assert result == [b"2", b"3", b"4"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro(key, limit=Limit(1, 3)) + assert result_ro == [b"2", b"3", b"4"] + + # order argument + result = await glide_client.sort(key, order=OrderBy.DESC) + assert result == [b"5", b"4", b"3", b"2", b"1"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro(key, order=OrderBy.DESC) + assert result_ro == [b"5", b"4", b"3", b"2", b"1"] + + assert await glide_client.lpush(key, ["a"]) == 6 + + with pytest.raises(RequestError) as e: + await glide_client.sort(key) + assert "can't be converted into double" in str(e).lower() + + if not skip_sort_ro_test: + with pytest.raises(RequestError) as e: + await glide_client.sort_ro(key) + assert "can't be converted into double" in str(e).lower() + + # alpha argument + result = await glide_client.sort(key, alpha=True) + assert result == [b"1", b"2", b"3", b"4", b"5", b"a"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro(key, alpha=True) + assert result_ro == [b"1", b"2", b"3", b"4", b"5", b"a"] + + # Combining multiple arguments + result = await glide_client.sort( + key, limit=Limit(1, 3), order=OrderBy.DESC, alpha=True + ) + assert result == [b"5", b"4", b"3"] + + if not skip_sort_ro_test: + result_ro = await glide_client.sort_ro( + key, limit=Limit(1, 3), order=OrderBy.DESC, alpha=True + ) + assert result_ro == [b"5", b"4", b"3"] + + # Test sort_store with combined arguments + sort_store_result = await glide_client.sort_store( + key, store, limit=Limit(1, 3), order=OrderBy.DESC, alpha=True + ) + assert sort_store_result == 3 + sorted_list = await glide_client.lrange(store, 0, -1) + assert sorted_list == [b"5", b"4", b"3"] + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_echo(self, glide_client: TGlideClient): + message = get_random_string(5) + assert await glide_client.echo(message) == message.encode() + if isinstance(glide_client, GlideClusterClient): + echo_dict = await glide_client.echo(message, AllNodes()) + assert isinstance(echo_dict, dict) + for value in echo_dict.values(): + assert value == message.encode() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_dbsize(self, glide_client: TGlideClient): + assert await glide_client.custom_command(["FLUSHALL"]) == OK + + assert await glide_client.dbsize() == 0 + key_value_pairs = [(get_random_string(10), "foo") for _ in range(10)] + + for key, value in key_value_pairs: + assert await glide_client.set(key, value) == OK + assert await glide_client.dbsize() == 10 + + if isinstance(glide_client, GlideClusterClient): + assert await glide_client.custom_command(["FLUSHALL"]) == OK + key = get_random_string(5) + assert await glide_client.set(key, value) == OK + assert await glide_client.dbsize(SlotKeyRoute(SlotType.PRIMARY, key)) == 1 + else: + assert await glide_client.select(1) == OK + assert await glide_client.dbsize() == 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_time(self, glide_client: TGlideClient): + current_time = int(time.time()) - 1 + result = await glide_client.time() + assert len(result) == 2 + assert isinstance(result, list) + assert int(result[0]) > current_time + assert 0 < int(result[1]) < 1000000 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lastsave(self, glide_client: TGlideClient): + yesterday = date.today() - timedelta(1) + yesterday_unix_time = time.mktime(yesterday.timetuple()) + + result = await glide_client.lastsave() + assert isinstance(result, int) + assert result > yesterday_unix_time + + if isinstance(glide_client, GlideClusterClient): + # test with single-node route + result = await glide_client.lastsave(RandomNode()) + assert isinstance(result, int) + assert result > yesterday_unix_time + + # test with multi-node route + result = await glide_client.lastsave(AllNodes()) + assert isinstance(result, dict) + for lastsave_time in result.values(): + assert lastsave_time > yesterday_unix_time + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_append(self, glide_client: TGlideClient): + key, value = get_random_string(10), get_random_string(5) + assert await glide_client.append(key, value) == 5 + + assert await glide_client.append(key, value) == 10 + assert await glide_client.get(key) == (value * 2).encode() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xadd_xtrim_xlen(self, glide_client: TGlideClient): + key = get_random_string(10) + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + field, field2 = get_random_string(10), get_random_string(10) + + assert ( + await glide_client.xadd( + key, + [(field, "foo"), (field2, "bar")], + StreamAddOptions(make_stream=False), + ) + is None + ) + + assert ( + await glide_client.xadd( + key, [(field, "foo1"), (field2, "bar1")], StreamAddOptions(id="0-1") + ) + == b"0-1" + ) + + assert ( + await glide_client.xadd(key, [(field, "foo2"), (field2, "bar2")]) + ) is not None + assert await glide_client.xlen(key) == 2 + + # This will trim the first entry. + id = await glide_client.xadd( + key, + [(field, "foo3"), (field2, "bar3")], + StreamAddOptions(trim=TrimByMaxLen(exact=True, threshold=2)), + ) + + assert id is not None + # TODO: remove when functions API is fixed + assert isinstance(id, bytes) + assert await glide_client.xlen(key) == 2 + + # This will trim the 2nd entry. + assert ( + await glide_client.xadd( + key, + [(field, "foo4"), (field2, "bar4")], + StreamAddOptions(trim=TrimByMinId(exact=True, threshold=id.decode())), + ) + is not None + ) + assert await glide_client.xlen(key) == 2 + + assert await glide_client.xtrim(key, TrimByMaxLen(threshold=1, exact=True)) == 1 + assert await glide_client.xlen(key) == 1 + + assert await glide_client.xtrim(key, TrimByMaxLen(threshold=0, exact=True)) == 1 + # Unlike other Valkey collection types, stream keys still exist even after removing all entries + assert await glide_client.exists([key]) == 1 + assert await glide_client.xlen(key) == 0 + + assert ( + await glide_client.xtrim( + non_existing_key, TrimByMaxLen(threshold=1, exact=True) + ) + == 0 + ) + assert await glide_client.xlen(non_existing_key) == 0 + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") + with pytest.raises(RequestError): + await glide_client.xtrim(string_key, TrimByMaxLen(threshold=1, exact=True)) + with pytest.raises(RequestError): + await glide_client.xlen(string_key) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xdel(self, glide_client: TGlideClient): + key1 = get_random_string(10) + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + stream_id1 = "0-1" + stream_id2 = "0-2" + stream_id3 = "0-3" + + assert ( + await glide_client.xadd( + key1, [("f1", "foo1"), ("f2", "foo2")], StreamAddOptions(stream_id1) + ) + == stream_id1.encode() + ) + assert ( + await glide_client.xadd( + key1, [("f1", "foo1"), ("f2", "foo2")], StreamAddOptions(stream_id2) + ) + == stream_id2.encode() + ) + assert await glide_client.xlen(key1) == 2 + + # deletes one stream id, and ignores anything invalid + assert await glide_client.xdel(key1, [stream_id1, stream_id3]) == 1 + assert await glide_client.xdel(non_existing_key, [stream_id3]) == 0 + + # invalid argument - id list should not be empty + with pytest.raises(RequestError): + await glide_client.xdel(key1, []) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xdel(string_key, [stream_id3]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xrange_and_xrevrange(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + string_key = get_random_string(10) + stream_id1 = "0-1" + stream_id2 = "0-2" + stream_id3 = "0-3" + + assert ( + await glide_client.xadd( + key, [("f1", "v1")], StreamAddOptions(id=stream_id1) + ) + == stream_id1.encode() + ) + assert ( + await glide_client.xadd( + key, [("f2", "v2")], StreamAddOptions(id=stream_id2) + ) + == stream_id2.encode() + ) + assert await glide_client.xlen(key) == 2 + + # get everything from the stream + result = await glide_client.xrange(key, MinId(), MaxId()) + assert convert_bytes_to_string_object(result) == { + stream_id1: [["f1", "v1"]], + stream_id2: [["f2", "v2"]], + } + result = await glide_client.xrevrange(key, MaxId(), MinId()) + assert convert_bytes_to_string_object(result) == { + stream_id2: [["f2", "v2"]], + stream_id1: [["f1", "v1"]], + } + + # returns empty mapping if + before - + assert await glide_client.xrange(key, MaxId(), MinId()) == {} + # rev search returns empty mapping if - before + + assert await glide_client.xrevrange(key, MinId(), MaxId()) == {} + + assert ( + await glide_client.xadd( + key, [("f3", "v3")], StreamAddOptions(id=stream_id3) + ) + == stream_id3.encode() + ) + + # get the newest entry + result = await glide_client.xrange( + key, ExclusiveIdBound(stream_id2), ExclusiveIdBound.from_timestamp(5), 1 + ) + assert convert_bytes_to_string_object(result) == {stream_id3: [["f3", "v3"]]} + result = await glide_client.xrevrange( + key, ExclusiveIdBound.from_timestamp(5), ExclusiveIdBound(stream_id2), 1 + ) + assert convert_bytes_to_string_object(result) == {stream_id3: [["f3", "v3"]]} + + # xrange/xrevrange against an emptied stream + assert await glide_client.xdel(key, [stream_id1, stream_id2, stream_id3]) == 3 + assert await glide_client.xrange(key, MinId(), MaxId(), 10) == {} + assert await glide_client.xrevrange(key, MaxId(), MinId(), 10) == {} + + assert await glide_client.xrange(non_existing_key, MinId(), MaxId()) == {} + assert await glide_client.xrevrange(non_existing_key, MaxId(), MinId()) == {} + + # count value < 1 returns None + assert await glide_client.xrange(key, MinId(), MaxId(), 0) is None + assert await glide_client.xrange(key, MinId(), MaxId(), -1) is None + assert await glide_client.xrevrange(key, MaxId(), MinId(), 0) is None + assert await glide_client.xrevrange(key, MaxId(), MinId(), -1) is None + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") + with pytest.raises(RequestError): + await glide_client.xrange(string_key, MinId(), MaxId()) + with pytest.raises(RequestError): + await glide_client.xrevrange(string_key, MaxId(), MinId()) + + # invalid start bound + with pytest.raises(RequestError): + await glide_client.xrange(key, IdBound("not_a_stream_id"), MaxId()) + with pytest.raises(RequestError): + await glide_client.xrevrange(key, MaxId(), IdBound("not_a_stream_id")) + + # invalid end bound + with pytest.raises(RequestError): + await glide_client.xrange(key, MinId(), IdBound("not_a_stream_id")) + with pytest.raises(RequestError): + await glide_client.xrevrange(key, IdBound("not_a_stream_id"), MinId()) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xread( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:3-{get_random_string(10)}" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + stream_id2_1 = "2-1" + stream_id2_2 = "2-2" + stream_id2_3 = "2-3" + non_existing_id = "99-99" + + # setup first entries in streams key1 and key2 + assert ( + await glide_client.xadd( + key1, [("f1_1", "v1_1")], StreamAddOptions(id=stream_id1_1) + ) + == stream_id1_1.encode() + ) + assert ( + await glide_client.xadd( + key2, [("f2_1", "v2_1")], StreamAddOptions(id=stream_id2_1) + ) + == stream_id2_1.encode() + ) + + # setup second entries in streams key1 and key2 + assert ( + await glide_client.xadd( + key1, [("f1_2", "v1_2")], StreamAddOptions(id=stream_id1_2) + ) + == stream_id1_2.encode() + ) + assert ( + await glide_client.xadd( + key2, [("f2_2", "v2_2")], StreamAddOptions(id=stream_id2_2) + ) + == stream_id2_2.encode() + ) + + # setup third entries in streams key1 and key2 + assert ( + await glide_client.xadd( + key1, [("f1_3", "v1_3")], StreamAddOptions(id=stream_id1_3) + ) + == stream_id1_3.encode() + ) + assert ( + await glide_client.xadd( + key2, [("f2_3", "v2_3")], StreamAddOptions(id=stream_id2_3) + ) + == stream_id2_3.encode() + ) + + assert await glide_client.xread({key1: stream_id1_1, key2: stream_id2_1}) == { + key1.encode(): { + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + stream_id1_3.encode(): [[b"f1_3", b"v1_3"]], + }, + key2.encode(): { + stream_id2_2.encode(): [[b"f2_2", b"v2_2"]], + stream_id2_3.encode(): [[b"f2_3", b"v2_3"]], + }, + } + + assert await glide_client.xread({non_existing_key: stream_id1_1}) is None + assert await glide_client.xread({key1: non_existing_id}) is None + + # passing an empty read options argument has no effect + assert await glide_client.xread({key1: stream_id1_1}, StreamReadOptions()) == { + key1.encode(): { + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + stream_id1_3.encode(): [[b"f1_3", b"v1_3"]], + }, + } + + assert await glide_client.xread( + {key1: stream_id1_1}, StreamReadOptions(count=1) + ) == { + key1.encode(): { + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + }, + } + assert await glide_client.xread( + {key1: stream_id1_1}, StreamReadOptions(count=1, block_ms=1000) + ) == { + key1.encode(): { + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + }, + } + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xread_edge_cases_and_failures( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + string_key = f"{{testKey}}:2-{get_random_string(10)}" + stream_id0 = "0-0" + stream_id1 = "1-1" + stream_id2 = "1-2" + + assert ( + await glide_client.xadd( + key1, [("f1", "v1")], StreamAddOptions(id=stream_id1) + ) + == stream_id1.encode() + ) + assert ( + await glide_client.xadd( + key1, [("f2", "v2")], StreamAddOptions(id=stream_id2) + ) + == stream_id2.encode() + ) + + test_client = await create_client( + request=request, protocol=protocol, cluster_mode=cluster_mode, timeout=900 + ) + # ensure command doesn't time out even if timeout > request timeout + assert ( + await test_client.xread( + {key1: stream_id2}, StreamReadOptions(block_ms=1000) + ) + is None + ) + + async def endless_xread_call(): + await test_client.xread({key1: stream_id2}, StreamReadOptions(block_ms=0)) + + # when xread is called with a block timeout of 0, it should never timeout, but we wrap the test with a timeout + # to avoid the test getting stuck forever. + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_xread_call(), timeout=3) + + # if count is non-positive, it is ignored + assert await glide_client.xread( + {key1: stream_id0}, StreamReadOptions(count=0) + ) == { + key1.encode(): { + stream_id1.encode(): [[b"f1", b"v1"]], + stream_id2.encode(): [[b"f2", b"v2"]], + }, + } + assert await glide_client.xread( + {key1: stream_id0}, StreamReadOptions(count=-1) + ) == { + key1.encode(): { + stream_id1.encode(): [[b"f1", b"v1"]], + stream_id2.encode(): [[b"f2", b"v2"]], + }, + } + + # invalid stream ID + with pytest.raises(RequestError): + await glide_client.xread({key1: "invalid_stream_id"}) + + # invalid argument - block cannot be negative + with pytest.raises(RequestError): + await glide_client.xread({key1: stream_id1}, StreamReadOptions(block_ms=-1)) + + # invalid argument - keys_and_ids must not be empty + with pytest.raises(RequestError): + await glide_client.xread({}) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") + with pytest.raises(RequestError): + await glide_client.xread({string_key: stream_id1, key1: stream_id1}) + with pytest.raises(RequestError): + await glide_client.xread({key1: stream_id1, string_key: stream_id1}) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xgroup_create_xgroup_destroy( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key = get_random_string(10) + non_existing_key = get_random_string(10) + string_key = get_random_string(10) + group_name1 = get_random_string(10) + group_name2 = get_random_string(10) + stream_id = "0-1" + + # trying to create a consumer group for a non-existing stream without the "MKSTREAM" arg results in error + with pytest.raises(RequestError): + await glide_client.xgroup_create(non_existing_key, group_name1, stream_id) + + # calling with the "MKSTREAM" arg should create the new stream automatically + assert ( + await glide_client.xgroup_create( + key, group_name1, stream_id, StreamGroupOptions(make_stream=True) + ) + == OK + ) + + # invalid arg - group names must be unique, but group_name1 already exists + with pytest.raises(RequestError): + await glide_client.xgroup_create(key, group_name1, stream_id) + + # invalid stream ID format + with pytest.raises(RequestError): + await glide_client.xgroup_create( + key, group_name2, "invalid_stream_id_format" + ) + + assert await glide_client.xgroup_destroy(key, group_name1) is True + # calling xgroup_destroy again returns False because the group was already destroyed above + assert await glide_client.xgroup_destroy(key, group_name1) is False + + # attempting to destroy a group for a non-existing key should raise an error + with pytest.raises(RequestError): + await glide_client.xgroup_destroy(non_existing_key, group_name1) + + # "ENTRIESREAD" option was added in Valkey 7.0.0 + if await check_if_server_version_lt(glide_client, "7.0.0"): + with pytest.raises(RequestError): + await glide_client.xgroup_create( + key, + group_name1, + stream_id, + StreamGroupOptions(entries_read=10), + ) + else: + assert ( + await glide_client.xgroup_create( + key, + group_name1, + stream_id, + StreamGroupOptions(entries_read=10), + ) + == OK + ) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xgroup_create( + string_key, group_name1, stream_id, StreamGroupOptions(make_stream=True) + ) + with pytest.raises(RequestError): + await glide_client.xgroup_destroy(string_key, group_name1) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xgroup_create_consumer_xreadgroup_xgroup_del_consumer( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + group_name = get_random_string(10) + consumer_name = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + + # create group and consumer for the group + assert ( + await glide_client.xgroup_create( + key, group_name, stream_id0, StreamGroupOptions(make_stream=True) + ) + == OK + ) + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer_name) + is True + ) + + # attempting to create/delete a consumer for a group that does not exist results in a NOGROUP request error + with pytest.raises(RequestError): + await glide_client.xgroup_create_consumer( + key, "non_existing_group", consumer_name + ) + with pytest.raises(RequestError): + await glide_client.xgroup_del_consumer( + key, "non_existing_group", consumer_name + ) + + # attempt to create consumer for group again + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer_name) + is False + ) + + # attempting to delete a consumer that has not been created yet returns 0 + assert ( + await glide_client.xgroup_del_consumer( + key, group_name, "non_existing_consumer" + ) + == 0 + ) + + # add two stream entries + assert ( + await glide_client.xadd( + key, [("f1_0", "v1_0")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_1", "v1_1")], StreamAddOptions(stream_id1_1) + ) + == stream_id1_1.encode() + ) + + # read the entire stream for the consumer and mark messages as pending + assert await glide_client.xreadgroup( + {key: ">"}, + group_name, + consumer_name, + StreamReadGroupOptions(block_ms=1000, count=10), + ) == { + key.encode(): { + stream_id1_0.encode(): [[b"f1_0", b"v1_0"]], + stream_id1_1.encode(): [[b"f1_1", b"v1_1"]], + } + } + + # delete one of the stream entries + assert await glide_client.xdel(key, [stream_id1_0]) == 1 + + # now xreadgroup yields one empty stream entry and one non-empty stream entry + assert await glide_client.xreadgroup({key: "0"}, group_name, consumer_name) == { + key.encode(): { + stream_id1_0.encode(): None, + stream_id1_1.encode(): [[b"f1_1", b"v1_1"]], + } + } + + assert ( + await glide_client.xadd( + key, [("f1_2", "v1_2")], StreamAddOptions(stream_id1_2) + ) + == stream_id1_2.encode() + ) + + # delete the consumer group and expect 2 pending messages + assert ( + await glide_client.xgroup_del_consumer(key, group_name, consumer_name) == 2 + ) + + # consume the last message with the previously deleted consumer (create the consumer anew) + assert await glide_client.xreadgroup( + {key: ">"}, + group_name, + consumer_name, + StreamReadGroupOptions(count=5, block_ms=1000), + ) == {key.encode(): {stream_id1_2.encode(): [[b"f1_2", b"v1_2"]]}} + + # delete the consumer group and expect the pending message + assert ( + await glide_client.xgroup_del_consumer(key, group_name, consumer_name) == 1 + ) + + # test NOACK option + assert ( + await glide_client.xadd( + key, [("f1_3", "v1_3")], StreamAddOptions(stream_id1_3) + ) + == stream_id1_3.encode() + ) + # since NOACK is passed, stream entry will be consumed without being added to the pending entries + assert await glide_client.xreadgroup( + {key: ">"}, + group_name, + consumer_name, + StreamReadGroupOptions(no_ack=True, count=5, block_ms=1000), + ) == {key.encode(): {stream_id1_3.encode(): [[b"f1_3", b"v1_3"]]}} + assert ( + await glide_client.xreadgroup( + {key: ">"}, + group_name, + consumer_name, + StreamReadGroupOptions(no_ack=False, count=5, block_ms=1000), + ) + is None + ) + assert await glide_client.xreadgroup( + {key: "0"}, + group_name, + consumer_name, + StreamReadGroupOptions(no_ack=False, count=5, block_ms=1000), + ) == {key.encode(): {}} + + # attempting to call XGROUP CREATECONSUMER or XGROUP DELCONSUMER with a non-existing key should raise an error + with pytest.raises(RequestError): + await glide_client.xgroup_create_consumer( + non_existing_key, group_name, consumer_name + ) + with pytest.raises(RequestError): + await glide_client.xgroup_del_consumer( + non_existing_key, group_name, consumer_name + ) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xgroup_create_consumer( + string_key, group_name, consumer_name + ) + with pytest.raises(RequestError): + await glide_client.xgroup_del_consumer( + string_key, group_name, consumer_name + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xreadgroup_edge_cases_and_failures( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + group_name = get_random_string(10) + consumer_name = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + + # attempting to execute against a non-existing key results in an error + with pytest.raises(RequestError): + await glide_client.xreadgroup( + {non_existing_key: stream_id0}, group_name, consumer_name + ) + + # create group and consumer for group + assert await glide_client.xgroup_create( + key, group_name, stream_id0, StreamGroupOptions(make_stream=True) + ) + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer_name) + is True + ) + + # read from empty stream + assert ( + await glide_client.xreadgroup({key: ">"}, group_name, consumer_name) is None + ) + assert await glide_client.xreadgroup({key: "0"}, group_name, consumer_name) == { + key.encode(): {} + } + + # setup first entry + assert ( + await glide_client.xadd(key, [("f1", "v1")], StreamAddOptions(stream_id1_1)) + == stream_id1_1.encode() + ) + + # if count is non-positive, it is ignored + assert await glide_client.xreadgroup( + {key: ">"}, group_name, consumer_name, StreamReadGroupOptions(count=0) + ) == { + key.encode(): { + stream_id1_1.encode(): [[b"f1", b"v1"]], + }, + } + assert await glide_client.xreadgroup( + {key: stream_id1_0}, + group_name, + consumer_name, + StreamReadGroupOptions(count=-1), + ) == { + key.encode(): { + stream_id1_1.encode(): [[b"f1", b"v1"]], + }, + } + + # invalid stream ID + with pytest.raises(RequestError): + await glide_client.xreadgroup( + {key: "invalid_stream_id"}, group_name, consumer_name + ) + + # invalid argument - block cannot be negative + with pytest.raises(RequestError): + await glide_client.xreadgroup( + {key: stream_id0}, + group_name, + consumer_name, + StreamReadGroupOptions(block_ms=-1), + ) + + # invalid argument - keys_and_ids must not be empty + with pytest.raises(RequestError): + await glide_client.xreadgroup({}, group_name, consumer_name) + + # first key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xreadgroup( + {string_key: stream_id1_1, key: stream_id1_1}, group_name, consumer_name + ) + + # second key exists, but it is not a stream + with pytest.raises(RequestError): + await glide_client.xreadgroup( + {key: stream_id1_1, string_key: stream_id1_1}, group_name, consumer_name + ) + + # attempting to execute command with a non-existing group results in an error + with pytest.raises(RequestError): + await glide_client.xreadgroup( + {key: stream_id1_1}, "non_existing_group", consumer_name + ) + + test_client = await create_client( + request=request, protocol=protocol, cluster_mode=cluster_mode, timeout=900 + ) + timeout_key = f"{{testKey}}:{get_random_string(10)}" + timeout_group_name = get_random_string(10) + timeout_consumer_name = get_random_string(10) + + # create a group read with the test client + # add a single stream entry and consumer + # the first call to ">" will return and update consumer group + # the second call to ">" will block waiting for new entries + # using anything other than ">" won't block, but will return the empty consumer result + # see: https://github.com/redis/redis/issues/6587 + assert ( + await test_client.xgroup_create( + timeout_key, + timeout_group_name, + stream_id0, + StreamGroupOptions(make_stream=True), + ) + == OK + ) + assert ( + await test_client.xgroup_create_consumer( + timeout_key, timeout_group_name, timeout_consumer_name + ) + is True + ) + assert ( + await test_client.xadd( + timeout_key, [("f1", "v1")], StreamAddOptions(stream_id1_1) + ) + == stream_id1_1.encode() + ) + + # read the entire stream for the consumer and mark messages as pending + assert await test_client.xreadgroup( + {timeout_key: ">"}, timeout_group_name, timeout_consumer_name + ) == {timeout_key.encode(): {stream_id1_1.encode(): [[b"f1", b"v1"]]}} + + # subsequent calls to read ">" will block + assert ( + await test_client.xreadgroup( + {timeout_key: ">"}, + timeout_group_name, + timeout_consumer_name, + StreamReadGroupOptions(block_ms=1000), + ) + is None + ) + + # ensure that command doesn't time out even if timeout > request timeout + async def endless_xreadgroup_call(): + await test_client.xreadgroup( + {timeout_key: ">"}, + timeout_group_name, + timeout_consumer_name, + StreamReadGroupOptions(block_ms=0), + ) + + # when xreadgroup is called with a block timeout of 0, it should never timeout, but we wrap the test with a + # timeout to avoid the test getting stuck forever. + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_xreadgroup_call(), timeout=3) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xack( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + group_name = get_random_string(10) + consumer_name = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + + # setup: add 2 entries to the stream, create consumer group, read to mark them as pending + assert ( + await glide_client.xadd(key, [("f0", "v0")], StreamAddOptions(stream_id1_0)) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd(key, [("f1", "v1")], StreamAddOptions(stream_id1_1)) + == stream_id1_1.encode() + ) + assert await glide_client.xgroup_create(key, group_name, stream_id0) == OK + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer_name) == { + key.encode(): { + stream_id1_0.encode(): [[b"f0", b"v0"]], + stream_id1_1.encode(): [[b"f1", b"v1"]], + } + } + + # add one more entry + assert ( + await glide_client.xadd(key, [("f2", "v2")], StreamAddOptions(stream_id1_2)) + == stream_id1_2.encode() + ) + + # acknowledge the first 2 entries + assert ( + await glide_client.xack(key, group_name, [stream_id1_0, stream_id1_1]) == 2 + ) + # attempting to acknowledge the first 2 entries again returns 0 since they were already acknowledged + assert ( + await glide_client.xack(key, group_name, [stream_id1_0, stream_id1_1]) == 0 + ) + # read the last, unacknowledged entry + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer_name) == { + key.encode(): {stream_id1_2.encode(): [[b"f2", b"v2"]]} + } + # deleting the consumer returns 1 since the last entry still hasn't been acknowledged + assert ( + await glide_client.xgroup_del_consumer(key, group_name, consumer_name) == 1 + ) + + # attempting to acknowledge a non-existing key returns 0 + assert ( + await glide_client.xack(non_existing_key, group_name, [stream_id1_0]) == 0 + ) + # attempting to acknowledge a non-existing group returns 0 + assert await glide_client.xack(key, "non_existing_group", [stream_id1_0]) == 0 + # attempting to acknowledge a non-existing ID returns 0 + assert await glide_client.xack(key, group_name, ["99-99"]) == 0 + + # invalid arg - ID list must not be empty + with pytest.raises(RequestError): + await glide_client.xack(key, group_name, []) + + # invalid arg - invalid stream ID format + with pytest.raises(RequestError): + await glide_client.xack(key, group_name, ["invalid_ID_format"]) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xack(string_key, group_name, [stream_id1_0]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xpending_xclaim(self, glide_client: TGlideClient): + key = get_random_string(10) + group_name = get_random_string(10) + consumer1 = get_random_string(10) + consumer2 = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + stream_id1_4 = "1-4" + stream_id1_5 = "1-5" + + # create group and consumer for group + assert ( + await glide_client.xgroup_create( + key, group_name, stream_id0, StreamGroupOptions(make_stream=True) + ) + == OK + ) + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer1) + is True + ) + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer2) + is True + ) + + # add two stream entries for consumer1 + assert ( + await glide_client.xadd( + key, [("f1_0", "v1_0")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_1", "v1_1")], StreamAddOptions(stream_id1_1) + ) + == stream_id1_1.encode() + ) + + # read the entire stream with consumer1 and mark messages as pending + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer1) == { + key.encode(): { + stream_id1_0.encode(): [[b"f1_0", b"v1_0"]], + stream_id1_1.encode(): [[b"f1_1", b"v1_1"]], + } + } + + # add three stream entries for consumer2 + assert ( + await glide_client.xadd( + key, [("f1_2", "v1_2")], StreamAddOptions(stream_id1_2) + ) + == stream_id1_2.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_3", "v1_3")], StreamAddOptions(stream_id1_3) + ) + == stream_id1_3.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_4", "v1_4")], StreamAddOptions(stream_id1_4) + ) + == stream_id1_4.encode() + ) + + # read the entire stream with consumer2 and mark messages as pending + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer2) == { + key.encode(): { + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + stream_id1_3.encode(): [[b"f1_3", b"v1_3"]], + stream_id1_4.encode(): [[b"f1_4", b"v1_4"]], + } + } + + # inner array order is non-deterministic, so we have to assert against it separately from the other info + result = await glide_client.xpending(key, group_name) + consumer_results = cast(List, result[3]) + assert [consumer1.encode(), b"2"] in consumer_results + assert [consumer2.encode(), b"3"] in consumer_results + + result.remove(consumer_results) + assert result == [5, stream_id1_0.encode(), stream_id1_4.encode()] + + # to ensure an idle_time > 0 + time.sleep(2) + range_result = await glide_client.xpending_range( + key, group_name, MinId(), MaxId(), 10 + ) + # the inner lists of the result have format [stream_entry_id, consumer, idle_time, times_delivered] + # because the idle time return value is not deterministic, we have to assert against it separately + idle_time = cast(int, range_result[0][2]) + assert idle_time > 0 + range_result[0].remove(idle_time) + assert range_result[0] == [stream_id1_0.encode(), consumer1.encode(), 1] + + idle_time = cast(int, range_result[1][2]) + assert idle_time > 0 + range_result[1].remove(idle_time) + assert range_result[1] == [stream_id1_1.encode(), consumer1.encode(), 1] + + idle_time = cast(int, range_result[2][2]) + assert idle_time > 0 + range_result[2].remove(idle_time) + assert range_result[2] == [stream_id1_2.encode(), consumer2.encode(), 1] + + idle_time = cast(int, range_result[3][2]) + assert idle_time > 0 + range_result[3].remove(idle_time) + assert range_result[3] == [stream_id1_3.encode(), consumer2.encode(), 1] + + idle_time = cast(int, range_result[4][2]) + assert idle_time > 0 + range_result[4].remove(idle_time) + assert range_result[4] == [stream_id1_4.encode(), consumer2.encode(), 1] + + # use xclaim to claim stream 2 and 4 for consumer 1 + assert await glide_client.xclaim( + key, group_name, consumer1, 0, [stream_id1_2, stream_id1_4] + ) == { + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + stream_id1_4.encode(): [[b"f1_4", b"v1_4"]], + } + + # claiming non exists id + assert ( + await glide_client.xclaim( + key, group_name, consumer1, 0, ["1526569498055-0"] + ) + == {} + ) + + assert await glide_client.xclaim_just_id( + key, group_name, consumer1, 0, [stream_id1_2, stream_id1_4] + ) == [stream_id1_2.encode(), stream_id1_4.encode()] + + # add one more stream + assert ( + await glide_client.xadd( + key, [("f1_5", "v1_5")], StreamAddOptions(stream_id1_5) + ) + == stream_id1_5.encode() + ) + + # using force, we can xclaim the message without reading it + claim_force_result = await glide_client.xclaim( + key, + group_name, + consumer2, + 0, + [stream_id1_5], + StreamClaimOptions(retry_count=99, is_force=True), + ) + assert claim_force_result == {stream_id1_5.encode(): [[b"f1_5", b"v1_5"]]} + + force_pending_result = await glide_client.xpending_range( + key, group_name, IdBound(stream_id1_5), IdBound(stream_id1_5), 1 + ) + assert force_pending_result[0][0] == stream_id1_5.encode() + assert force_pending_result[0][1] == consumer2.encode() + assert force_pending_result[0][3] == 99 + + # acknowledge streams 1-1, 1-2, 1-3, 1-5 and remove them from the xpending results + assert ( + await glide_client.xack( + key, + group_name, + [stream_id1_1, stream_id1_2, stream_id1_3, stream_id1_5], + ) + == 4 + ) + + range_result = await glide_client.xpending_range( + key, group_name, IdBound(stream_id1_4), MaxId(), 10 + ) + assert len(range_result) == 1 + assert range_result[0][0] == stream_id1_4.encode() + assert range_result[0][1] == consumer1.encode() + + range_result = await glide_client.xpending_range( + key, group_name, MinId(), IdBound(stream_id1_3), 10 + ) + assert len(range_result) == 1 + assert range_result[0][0] == stream_id1_0.encode() + assert range_result[0][1] == consumer1.encode() + + # passing an empty StreamPendingOptions object should have no effect + range_result = await glide_client.xpending_range( + key, group_name, MinId(), IdBound(stream_id1_3), 10, StreamPendingOptions() + ) + assert len(range_result) == 1 + assert range_result[0][0] == stream_id1_0.encode() + assert range_result[0][1] == consumer1.encode() + + range_result = await glide_client.xpending_range( + key, + group_name, + MinId(), + MaxId(), + 10, + StreamPendingOptions(min_idle_time_ms=1, consumer_name=consumer1), + ) + # note: streams ID 0-0 and 0-4 are still pending, all others were acknowledged + assert len(range_result) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xpending_edge_cases_and_failures(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + string_key = get_random_string(10) + group_name = get_random_string(10) + consumer = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + + # create group and consumer for the group + assert ( + await glide_client.xgroup_create( + key, group_name, stream_id0, StreamGroupOptions(make_stream=True) + ) + == OK + ) + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer) is True + ) + + # add two stream entries for consumer + assert ( + await glide_client.xadd( + key, [("f1_0", "v1_0")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_1", "v1_1")], StreamAddOptions(stream_id1_1) + ) + == stream_id1_1.encode() + ) + + # no pending messages yet... + assert await glide_client.xpending(key, group_name) == [0, None, None, None] + assert ( + await glide_client.xpending_range(key, group_name, MinId(), MaxId(), 10) + == [] + ) + + # read the entire stream with consumer and mark messages as pending + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer) == { + key.encode(): { + stream_id1_0.encode(): [[b"f1_0", b"v1_0"]], + stream_id1_1.encode(): [[b"f1_1", b"v1_1"]], + } + } + + # sanity check - expect some results + assert await glide_client.xpending(key, group_name) == [ + 2, + stream_id1_0.encode(), + stream_id1_1.encode(), + [[consumer.encode(), b"2"]], + ] + result = await glide_client.xpending_range( + key, group_name, MinId(), MaxId(), 10 + ) + assert len(result[0]) > 0 + + # returns empty if + before - + assert ( + await glide_client.xpending_range(key, group_name, MaxId(), MinId(), 10) + == [] + ) + assert ( + await glide_client.xpending_range( + key, + group_name, + MaxId(), + MinId(), + 10, + StreamPendingOptions(consumer_name=consumer), + ) + == [] + ) + + # min idle time of 100 seconds shouldn't produce any results + assert ( + await glide_client.xpending_range( + key, + group_name, + MinId(), + MaxId(), + 10, + StreamPendingOptions(min_idle_time_ms=100_000), + ) + == [] + ) + + # non-existing consumer: no results + assert ( + await glide_client.xpending_range( + key, + group_name, + MinId(), + MaxId(), + 10, + StreamPendingOptions(consumer_name="non_existing_consumer"), + ) + == [] + ) + + # xpending when range bound is not a valid ID raises a RequestError + with pytest.raises(RequestError): + await glide_client.xpending_range( + key, group_name, IdBound("invalid_stream_id_format"), MaxId(), 10 + ) + with pytest.raises(RequestError): + await glide_client.xpending_range( + key, group_name, MinId(), IdBound("invalid_stream_id_format"), 10 + ) + + # non-positive count returns no results + assert ( + await glide_client.xpending_range(key, group_name, MinId(), MaxId(), -10) + == [] + ) + assert ( + await glide_client.xpending_range(key, group_name, MinId(), MaxId(), 0) + == [] + ) + + # non-positive min-idle-time values are allowed + result = await glide_client.xpending_range( + key, + group_name, + MinId(), + MaxId(), + 10, + StreamPendingOptions(min_idle_time_ms=-100), + ) + assert len(result[0]) > 0 + result = await glide_client.xpending_range( + key, + group_name, + MinId(), + MaxId(), + 10, + StreamPendingOptions(min_idle_time_ms=0), + ) + assert len(result[0]) > 0 + + # non-existing group name raises a RequestError (NOGROUP) + with pytest.raises(RequestError): + await glide_client.xpending(key, "non_existing_group") + with pytest.raises(RequestError): + await glide_client.xpending_range( + key, "non_existing_group", MinId(), MaxId(), 10 + ) + + # non-existing key raises a RequestError + with pytest.raises(RequestError): + await glide_client.xpending(non_existing_key, group_name) + with pytest.raises(RequestError): + await glide_client.xpending_range( + non_existing_key, group_name, MinId(), MaxId(), 10 + ) + + # key exists but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xpending(string_key, group_name) + with pytest.raises(RequestError): + await glide_client.xpending_range( + string_key, group_name, MinId(), MaxId(), 10 + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xclaim_edge_cases_and_failures(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + string_key = get_random_string(10) + group_name = get_random_string(10) + consumer = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + + # create group and consumer for the group + assert ( + await glide_client.xgroup_create( + key, group_name, stream_id0, StreamGroupOptions(make_stream=True) + ) + == OK + ) + assert ( + await glide_client.xgroup_create_consumer(key, group_name, consumer) is True + ) + + # Add stream entry and mark as pending: + assert ( + await glide_client.xadd( + key, [("f1_0", "v1_0")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + + # read the entire stream with consumer and mark messages as pending + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer) == { + key.encode(): {stream_id1_0.encode(): [[b"f1_0", b"v1_0"]]} + } + + # claim with invalid stream entry IDs + with pytest.raises(RequestError): + await glide_client.xclaim_just_id(key, group_name, consumer, 1, ["invalid"]) + + # claim with empty stream entry IDs returns no results + empty_claim = await glide_client.xclaim_just_id( + key, group_name, consumer, 1, [] + ) + assert len(empty_claim) == 0 + + claim_options = StreamClaimOptions(idle=1) + + # non-existent key throws a RequestError (NOGROUP) + with pytest.raises(RequestError) as e: + await glide_client.xclaim( + non_existing_key, group_name, consumer, 1, [stream_id1_0] + ) + assert "NOGROUP" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.xclaim( + non_existing_key, + group_name, + consumer, + 1, + [stream_id1_0], + claim_options, + ) + assert "NOGROUP" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.xclaim_just_id( + non_existing_key, group_name, consumer, 1, [stream_id1_0] + ) + assert "NOGROUP" in str(e) + + with pytest.raises(RequestError) as e: + await glide_client.xclaim_just_id( + non_existing_key, + group_name, + consumer, + 1, + [stream_id1_0], + claim_options, + ) + assert "NOGROUP" in str(e) + + # key exists but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xclaim( + string_key, group_name, consumer, 1, [stream_id1_0] + ) + with pytest.raises(RequestError): + await glide_client.xclaim( + string_key, group_name, consumer, 1, [stream_id1_0], claim_options + ) + with pytest.raises(RequestError): + await glide_client.xclaim_just_id( + string_key, group_name, consumer, 1, [stream_id1_0] + ) + with pytest.raises(RequestError): + await glide_client.xclaim_just_id( + string_key, group_name, consumer, 1, [stream_id1_0], claim_options + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xautoclaim(self, glide_client: TGlideClient, protocol): + min_version = "6.2.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + if await check_if_server_version_lt(glide_client, "7.0.0"): + version7_or_above = False + else: + version7_or_above = True + + key = get_random_string(10) + group_name = get_random_string(10) + consumer = get_random_string(10) + stream_id0_0 = "0-0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + + # setup: add stream entries, create consumer group, add entries to Pending Entries List for group + assert ( + await glide_client.xadd( + key, [("f1", "v1"), ("f2", "v2")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_1", "v1_1")], StreamAddOptions(stream_id1_1) + ) + == stream_id1_1.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_2", "v1_2")], StreamAddOptions(stream_id1_2) + ) + == stream_id1_2.encode() + ) + assert ( + await glide_client.xadd( + key, [("f1_3", "v1_3")], StreamAddOptions(stream_id1_3) + ) + == stream_id1_3.encode() + ) + assert await glide_client.xgroup_create(key, group_name, stream_id0_0) == OK + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer) == { + key.encode(): { + stream_id1_0.encode(): [[b"f1", b"v1"], [b"f2", b"v2"]], + stream_id1_1.encode(): [[b"f1_1", b"v1_1"]], + stream_id1_2.encode(): [[b"f1_2", b"v1_2"]], + stream_id1_3.encode(): [[b"f1_3", b"v1_3"]], + } + } + + # autoclaim the first entry only + result = await glide_client.xautoclaim( + key, group_name, consumer, 0, stream_id0_0, count=1 + ) + assert result[0] == stream_id1_1.encode() + assert result[1] == {stream_id1_0.encode(): [[b"f1", b"v1"], [b"f2", b"v2"]]} + # if using Valkey 7.0.0 or above, responses also include a list of entry IDs that were removed from the Pending + # Entries List because they no longer exist in the stream + if version7_or_above: + assert result[2] == [] + + # delete entry 1-2 + assert await glide_client.xdel(key, [stream_id1_2]) + + # autoclaim the rest of the entries + result = await glide_client.xautoclaim( + key, group_name, consumer, 0, stream_id1_1 + ) + assert ( + result[0] == stream_id0_0.encode() + ) # "0-0" is returned to indicate the entire stream was scanned. + assert result[1] == { + stream_id1_1.encode(): [[b"f1_1", b"v1_1"]], + stream_id1_3.encode(): [[b"f1_3", b"v1_3"]], + } + if version7_or_above: + assert result[2] == [stream_id1_2.encode()] + + # autoclaim with JUSTID: result at index 1 does not contain fields/values of the claimed entries, only IDs + just_id_result = await glide_client.xautoclaim_just_id( + key, group_name, consumer, 0, stream_id0_0 + ) + assert just_id_result[0] == stream_id0_0.encode() + if version7_or_above: + assert just_id_result[1] == [ + stream_id1_0.encode(), + stream_id1_1.encode(), + stream_id1_3.encode(), + ] + assert just_id_result[2] == [] + else: + # in Valkey < 7.0.0, specifically for XAUTOCLAIM with JUSTID, entry IDs that were in the Pending Entries List + # but are no longer in the stream still show up in the response + assert just_id_result[1] == [ + stream_id1_0.encode(), + stream_id1_1.encode(), + stream_id1_2.encode(), + stream_id1_3.encode(), + ] + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xautoclaim_edge_cases_and_failures( + self, glide_client: TGlideClient, protocol + ): + min_version = "6.2.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + if await check_if_server_version_lt(glide_client, "7.0.0"): + version7_or_above = False + else: + version7_or_above = True + + key = get_random_string(10) + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + group_name = get_random_string(10) + consumer = get_random_string(10) + stream_id0_0 = "0-0" + stream_id1_0 = "1-0" + + # setup: add entry, create consumer group, add entry to Pending Entries List for group + assert ( + await glide_client.xadd(key, [("f1", "v1")], StreamAddOptions(stream_id1_0)) + == stream_id1_0.encode() + ) + assert await glide_client.xgroup_create(key, group_name, stream_id0_0) == OK + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer) == { + key.encode(): {stream_id1_0.encode(): [[b"f1", b"v1"]]} + } + + # passing a non-existing key is not allowed and will raise an error + with pytest.raises(RequestError): + await glide_client.xautoclaim( + non_existing_key, group_name, consumer, 0, stream_id0_0 + ) + with pytest.raises(RequestError): + await glide_client.xautoclaim_just_id( + non_existing_key, group_name, consumer, 0, stream_id0_0 + ) + + # passing a non-existing group is not allowed and will raise an error + with pytest.raises(RequestError): + await glide_client.xautoclaim( + key, "non_existing_group", consumer, 0, stream_id0_0 + ) + with pytest.raises(RequestError): + await glide_client.xautoclaim_just_id( + key, "non_existing_group", consumer, 0, stream_id0_0 + ) + + # non-existing consumers are created automatically + result = await glide_client.xautoclaim( + key, group_name, "non_existing_consumer", 0, stream_id0_0 + ) + assert result[0] == stream_id0_0.encode() + assert result[1] == {stream_id1_0.encode(): [[b"f1", b"v1"]]} + # if using Valkey 7.0.0 or above, responses also include a list of entry IDs that were removed from the Pending + # Entries List because they no longer exist in the stream + if version7_or_above: + assert result[2] == [] + + just_id_result = await glide_client.xautoclaim_just_id( + key, group_name, "non_existing_consumer", 0, stream_id0_0 + ) + assert just_id_result[0] == stream_id0_0.encode() + assert just_id_result[1] == [stream_id1_0.encode()] + if version7_or_above: + assert just_id_result[2] == [] + + # negative min_idle_time_ms values are allowed + result = await glide_client.xautoclaim( + key, group_name, consumer, -1, stream_id0_0 + ) + assert result[0] == stream_id0_0.encode() + assert result[1] == {stream_id1_0.encode(): [[b"f1", b"v1"]]} + if version7_or_above: + assert result[2] == [] + + just_id_result = await glide_client.xautoclaim_just_id( + key, group_name, consumer, -1, stream_id0_0 + ) + assert just_id_result[0] == stream_id0_0.encode() + assert just_id_result[1] == [stream_id1_0.encode()] + if version7_or_above: + assert just_id_result[2] == [] + + with pytest.raises(RequestError): + await glide_client.xautoclaim( + key, group_name, consumer, 0, "invalid_stream_id" + ) + with pytest.raises(RequestError): + await glide_client.xautoclaim_just_id( + key, group_name, consumer, 0, "invalid_stream_id" + ) + + # no stream entries to claim above the given start value + result = await glide_client.xautoclaim(key, group_name, consumer, 0, "99-99") + assert result[0] == stream_id0_0.encode() + assert result[1] == {} + if version7_or_above: + assert result[2] == [] + + just_id_result = await glide_client.xautoclaim_just_id( + key, group_name, consumer, 0, "99-99" + ) + assert just_id_result[0] == stream_id0_0.encode() + assert just_id_result[1] == [] + if version7_or_above: + assert just_id_result[2] == [] + + # invalid arg - count must be positive + with pytest.raises(RequestError): + await glide_client.xautoclaim( + key, group_name, consumer, 0, stream_id0_0, count=0 + ) + with pytest.raises(RequestError): + await glide_client.xautoclaim_just_id( + key, group_name, consumer, 0, stream_id0_0, count=0 + ) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xautoclaim( + string_key, group_name, consumer, 0, stream_id0_0 + ) + with pytest.raises(RequestError): + await glide_client.xautoclaim_just_id( + string_key, group_name, consumer, 0, stream_id0_0 + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xinfo_groups_xinfo_consumers( + self, glide_client: TGlideClient, protocol + ): + key = get_random_string(10) + group_name1 = get_random_string(10) + group_name2 = get_random_string(10) + consumer1 = get_random_string(10) + consumer2 = get_random_string(10) + stream_id0_0 = "0-0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + + # setup: add 3 entries to stream, create consumer group and consumer1, read 1 entry from stream with consumer1 + assert ( + await glide_client.xadd( + key, [("f1", "v1"), ("f2", "v2")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd(key, [("f3", "v3")], StreamAddOptions(stream_id1_1)) + == stream_id1_1.encode() + ) + assert ( + await glide_client.xadd(key, [("f4", "v4")], StreamAddOptions(stream_id1_2)) + == stream_id1_2.encode() + ) + assert await glide_client.xgroup_create(key, group_name1, stream_id0_0) == OK + assert await glide_client.xreadgroup( + {key: ">"}, group_name1, consumer1, StreamReadGroupOptions(count=1) + ) == {key.encode(): {stream_id1_0.encode(): [[b"f1", b"v1"], [b"f2", b"v2"]]}} + + # sleep to ensure the idle time value and inactive time value returned by xinfo_consumers is > 0 + time.sleep(2) + consumers_result = await glide_client.xinfo_consumers(key, group_name1) + assert len(consumers_result) == 1 + consumer1_info = consumers_result[0] + assert consumer1_info.get(b"name") == consumer1.encode() + assert consumer1_info.get(b"pending") == 1 + assert cast(int, consumer1_info.get(b"idle")) > 0 + if not await check_if_server_version_lt(glide_client, "7.2.0"): + assert ( + cast(int, consumer1_info.get(b"inactive")) + > 0 # "inactive" was added in Valkey 7.2.0 + ) + + # create consumer2 and read the rest of the entries with it + assert ( + await glide_client.xgroup_create_consumer(key, group_name1, consumer2) + is True + ) + assert await glide_client.xreadgroup({key: ">"}, group_name1, consumer2) == { + key.encode(): { + stream_id1_1.encode(): [[b"f3", b"v3"]], + stream_id1_2.encode(): [[b"f4", b"v4"]], + } + } + + # verify that xinfo_consumers contains info for 2 consumers now + # test with byte string args + consumers_result = await glide_client.xinfo_consumers( + key.encode(), group_name1.encode() + ) + assert len(consumers_result) == 2 + + # add one more entry + assert ( + await glide_client.xadd(key, [("f5", "v5")], StreamAddOptions(stream_id1_3)) + == stream_id1_3.encode() + ) + + groups = await glide_client.xinfo_groups(key) + assert len(groups) == 1 + group1_info = groups[0] + assert group1_info.get(b"name") == group_name1.encode() + assert group1_info.get(b"consumers") == 2 + assert group1_info.get(b"pending") == 3 + assert group1_info.get(b"last-delivered-id") == stream_id1_2.encode() + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert ( + group1_info.get(b"entries-read") + == 3 # we have read stream entries 1-0, 1-1, and 1-2 + ) + assert ( + group1_info.get(b"lag") + == 1 # we still have not read one entry in the stream, entry 1-3 + ) + + # verify xgroup_set_id effects the returned value from xinfo_groups + assert await glide_client.xgroup_set_id(key, group_name1, stream_id1_1) == OK + # test with byte string arg + groups = await glide_client.xinfo_groups(key.encode()) + assert len(groups) == 1 + group1_info = groups[0] + assert group1_info.get(b"name") == group_name1.encode() + assert group1_info.get(b"consumers") == 2 + assert group1_info.get(b"pending") == 3 + assert group1_info.get(b"last-delivered-id") == stream_id1_1.encode() + # entries-read and lag were added to the result in 7.0.0 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + assert ( + group1_info.get(b"entries-read") + is None # gets set to None when we change the last delivered ID + ) + assert ( + group1_info.get(b"lag") + is None # gets set to None when we change the last delivered ID + ) + + if not await check_if_server_version_lt(glide_client, "7.0.0"): + # verify xgroup_set_id with entries_read effects the returned value from xinfo_groups + assert ( + await glide_client.xgroup_set_id( + key, group_name1, stream_id1_1, entries_read=1 + ) + == OK + ) + groups = await glide_client.xinfo_groups(key) + assert len(groups) == 1 + group1_info = groups[0] + assert group1_info.get(b"name") == group_name1.encode() + assert group1_info.get(b"consumers") == 2 + assert group1_info.get(b"pending") == 3 + assert group1_info.get(b"last-delivered-id") == stream_id1_1.encode() + assert group1_info.get(b"entries-read") == 1 + assert ( + group1_info.get(b"lag") + == 3 # lag is calculated as number of stream entries minus entries-read + ) + + # add one more consumer group + assert await glide_client.xgroup_create(key, group_name2, stream_id0_0) == OK + + # verify that xinfo_groups contains info for 2 consumer groups now + groups = await glide_client.xinfo_groups(key) + assert len(groups) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xinfo_groups_xinfo_consumers_edge_cases_and_failures( + self, glide_client: TGlideClient, protocol + ): + key = get_random_string(10) + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + group_name = get_random_string(10) + stream_id1_0 = "1-0" + + # passing a non-existing key raises an error + with pytest.raises(RequestError): + await glide_client.xinfo_groups(non_existing_key) + with pytest.raises(RequestError): + await glide_client.xinfo_consumers(non_existing_key, group_name) + + assert ( + await glide_client.xadd( + key, [("f1", "v1"), ("f2", "v2")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + + # passing a non-existing group raises an error + with pytest.raises(RequestError): + await glide_client.xinfo_consumers(key, "non_existing_group") + + # no groups exist yet + assert await glide_client.xinfo_groups(key) == [] + + assert await glide_client.xgroup_create(key, group_name, stream_id1_0) == OK + # no consumers exist yet + assert await glide_client.xinfo_consumers(key, group_name) == [] + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xinfo_groups(string_key) + with pytest.raises(RequestError): + await glide_client.xinfo_consumers(string_key, group_name) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xinfo_stream( + self, glide_client: TGlideClient, cluster_mode, protocol + ): + key = get_random_string(10) + group_name = get_random_string(10) + consumer = get_random_string(10) + stream_id0_0 = "0-0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + + # setup: add stream entry, create consumer group and consumer, read from stream with consumer + assert ( + await glide_client.xadd( + key, [("a", "b"), ("c", "d")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert await glide_client.xgroup_create(key, group_name, stream_id0_0) == OK + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer) == { + key.encode(): {stream_id1_0.encode(): [[b"a", b"b"], [b"c", b"d"]]} + } + + result = await glide_client.xinfo_stream(key) + assert result.get(b"length") == 1 + expected_first_entry = [stream_id1_0.encode(), [b"a", b"b", b"c", b"d"]] + assert result.get(b"first-entry") == expected_first_entry + + # only one entry exists, so first and last entry should be the same + assert result.get(b"last-entry") == expected_first_entry + + # call XINFO STREAM with a byte string arg + result2 = await glide_client.xinfo_stream(key.encode()) + assert result2 == result + + # add one more entry + assert ( + await glide_client.xadd( + key, [("foo", "bar")], StreamAddOptions(stream_id1_1) + ) + == stream_id1_1.encode() + ) + + result_full = await glide_client.xinfo_stream_full(key, count=1) + assert result_full.get(b"length") == 2 + entries = cast(list, result_full.get(b"entries")) + # only the first entry will be returned since we passed count=1 + assert len(entries) == 1 + assert entries[0] == expected_first_entry + + groups = cast(list, result_full.get(b"groups")) + assert len(groups) == 1 + group_info = groups[0] + assert group_info.get(b"name") == group_name.encode() + pending = group_info.get(b"pending") + assert len(pending) == 1 + assert stream_id1_0.encode() in pending[0] + + consumers = group_info.get(b"consumers") + assert len(consumers) == 1 + consumer_info = consumers[0] + assert consumer_info.get(b"name") == consumer.encode() + consumer_pending = consumer_info.get(b"pending") + assert len(consumer_pending) == 1 + assert stream_id1_0.encode() in consumer_pending[0] + + # call XINFO STREAM FULL with byte arg + result_full2 = await glide_client.xinfo_stream_full(key.encode()) + # 2 entries should be returned, since we didn't pass the COUNT arg this time + assert len(cast(list, result_full2.get(b"entries"))) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xinfo_stream_edge_cases_and_failures( + self, glide_client: TGlideClient, cluster_mode, protocol + ): + key = get_random_string(10) + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + stream_id1_0 = "1-0" + + # setup: create empty stream + assert ( + await glide_client.xadd( + key, [("field", "value")], StreamAddOptions(stream_id1_0) + ) + == stream_id1_0.encode() + ) + assert await glide_client.xdel(key, [stream_id1_0]) == 1 + + # XINFO STREAM called against empty stream + result = await glide_client.xinfo_stream(key) + assert result.get(b"length") == 0 + assert result.get(b"first-entry") is None + assert result.get(b"last-entry") is None + + # XINFO STREAM FULL called against empty stream. Negative count values are ignored. + result_full = await glide_client.xinfo_stream_full(key, count=-3) + assert result_full.get(b"length") == 0 + assert result_full.get(b"entries") == [] + assert result_full.get(b"groups") == [] + + # calling XINFO STREAM with a non-existing key raises an error + with pytest.raises(RequestError): + await glide_client.xinfo_stream(non_existing_key) + with pytest.raises(RequestError): + await glide_client.xinfo_stream_full(non_existing_key) + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") + with pytest.raises(RequestError): + await glide_client.xinfo_stream(string_key) + with pytest.raises(RequestError): + await glide_client.xinfo_stream_full(string_key) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xgroup_set_id( + self, glide_client: TGlideClient, cluster_mode, protocol, request + ): + key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + group_name = get_random_string(10) + consumer_name = get_random_string(10) + stream_id0 = "0" + stream_id1_0 = "1-0" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + + # setup: create stream with 3 entries, create consumer group, read entries to add them to the Pending Entries + # List + assert ( + await glide_client.xadd(key, [("f0", "v0")], StreamAddOptions(stream_id1_0)) + == stream_id1_0.encode() + ) + assert ( + await glide_client.xadd(key, [("f1", "v1")], StreamAddOptions(stream_id1_1)) + == stream_id1_1.encode() + ) + assert ( + await glide_client.xadd(key, [("f2", "v2")], StreamAddOptions(stream_id1_2)) + == stream_id1_2.encode() + ) + assert await glide_client.xgroup_create(key, group_name, stream_id0) == OK + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer_name) == { + key.encode(): { + stream_id1_0.encode(): [[b"f0", b"v0"]], + stream_id1_1.encode(): [[b"f1", b"v1"]], + stream_id1_2.encode(): [[b"f2", b"v2"]], + } + } + # sanity check: xreadgroup should not return more entries since they're all already in the Pending Entries List + assert ( + await glide_client.xreadgroup({key: ">"}, group_name, consumer_name) is None + ) + + # reset the last delivered ID for the consumer group to "1-1" + # ENTRIESREAD is only supported in Redis version 7.0.0 and above + if await check_if_server_version_lt(glide_client, "7.0.0"): + assert await glide_client.xgroup_set_id(key, group_name, stream_id1_1) == OK + else: + assert ( + await glide_client.xgroup_set_id( + key, group_name, stream_id1_1, entries_read=0 + ) + == OK + ) + + # xreadgroup should only return entry 1-2 since we reset the last delivered ID to 1-1 + assert await glide_client.xreadgroup({key: ">"}, group_name, consumer_name) == { + key.encode(): { + stream_id1_2.encode(): [[b"f2", b"v2"]], + } + } + + # an error is raised if XGROUP SETID is called with a non-existing key + with pytest.raises(RequestError): + await glide_client.xgroup_set_id(non_existing_key, group_name, stream_id0) + + # an error is raised if XGROUP SETID is called with a non-existing group + with pytest.raises(RequestError): + await glide_client.xgroup_set_id(key, "non_existing_group", stream_id0) + + # setting the ID to a non-existing ID is allowed + assert await glide_client.xgroup_set_id(key, group_name, "99-99") == OK + + # key exists, but it is not a stream + assert await glide_client.set(string_key, "foo") == OK + with pytest.raises(RequestError): + await glide_client.xgroup_set_id(string_key, group_name, stream_id0) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_pfadd(self, glide_client: TGlideClient): + key = get_random_string(10) + assert await glide_client.pfadd(key, []) == 1 + assert await glide_client.pfadd(key, ["one", "two"]) == 1 + assert await glide_client.pfadd(key, ["two"]) == 0 + assert await glide_client.pfadd(key, []) == 0 + + assert await glide_client.set("foo", "value") == OK + with pytest.raises(RequestError): + await glide_client.pfadd("foo", []) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_pfcount(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + assert await glide_client.pfadd(key1, ["a", "b", "c"]) == 1 + assert await glide_client.pfadd(key2, ["b", "c", "d"]) == 1 + assert await glide_client.pfcount([key1]) == 3 + assert await glide_client.pfcount([key2]) == 3 + assert await glide_client.pfcount([key1, key2]) == 4 + assert await glide_client.pfcount([key1, key2, non_existing_key]) == 4 + # empty HyperLogLog data set + assert await glide_client.pfadd(key3, []) == 1 + assert await glide_client.pfcount([key3]) == 0 + + # incorrect argument - key list cannot be empty + with pytest.raises(RequestError): + await glide_client.pfcount([]) + + # key exists, but it is not a HyperLogLog + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.pfcount([string_key]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_pfmerge(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + assert await glide_client.pfadd(key1, ["a", "b", "c"]) == 1 + assert await glide_client.pfadd(key2, ["b", "c", "d"]) == 1 + + # merge into new HyperLogLog data set + assert await glide_client.pfmerge(key3, [key1, key2]) == OK + assert await glide_client.pfcount([key3]) == 4 + + # merge into existing HyperLogLog data set + assert await glide_client.pfmerge(key1, [key2]) == OK + assert await glide_client.pfcount([key1]) == 4 + + # non-existing source key + assert await glide_client.pfmerge(key2, [key1, non_existing_key]) == OK + assert await glide_client.pfcount([key2]) == 4 + + # empty source key list + assert await glide_client.pfmerge(key1, []) == OK + assert await glide_client.pfcount([key1]) == 4 + + # source key exists, but it is not a HyperLogLog + assert await glide_client.set(string_key, "foo") + with pytest.raises(RequestError): + assert await glide_client.pfmerge(key3, [string_key]) + + # destination key exists, but it is not a HyperLogLog + with pytest.raises(RequestError): + assert await glide_client.pfmerge(string_key, [key3]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitcount(self, glide_client: TGlideClient): + key1 = get_random_string(10) + set_key = get_random_string(10) + non_existing_key = get_random_string(10) + value = "foobar" + + assert await glide_client.set(key1, value) == OK + assert await glide_client.bitcount(key1) == 26 + assert await glide_client.bitcount(key1, OffsetOptions(1, 1)) == 6 + assert await glide_client.bitcount(key1, OffsetOptions(0, -5)) == 10 + assert await glide_client.bitcount(non_existing_key, OffsetOptions(5, 30)) == 0 + assert await glide_client.bitcount(non_existing_key) == 0 + + # key exists, but it is not a string + assert await glide_client.sadd(set_key, [value]) == 1 + with pytest.raises(RequestError): + await glide_client.bitcount(set_key) + with pytest.raises(RequestError): + await glide_client.bitcount(set_key, OffsetOptions(1, 1)) + + if await check_if_server_version_lt(glide_client, "7.0.0"): + # exception thrown because BIT and BYTE options were implemented after 7.0.0 + with pytest.raises(RequestError): + await glide_client.bitcount( + key1, OffsetOptions(2, 5, BitmapIndexType.BYTE) + ) + with pytest.raises(RequestError): + await glide_client.bitcount( + key1, OffsetOptions(2, 5, BitmapIndexType.BIT) + ) + else: + assert ( + await glide_client.bitcount( + key1, OffsetOptions(2, 5, BitmapIndexType.BYTE) + ) + == 16 + ) + assert ( + await glide_client.bitcount( + key1, OffsetOptions(5, 30, BitmapIndexType.BIT) + ) + == 17 + ) + assert ( + await glide_client.bitcount( + key1, OffsetOptions(5, -5, BitmapIndexType.BIT) + ) + == 23 + ) + assert ( + await glide_client.bitcount( + non_existing_key, OffsetOptions(5, 30, BitmapIndexType.BIT) + ) + == 0 + ) + + # key exists but it is not a string + with pytest.raises(RequestError): + await glide_client.bitcount( + set_key, OffsetOptions(1, 1, BitmapIndexType.BIT) + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_setbit(self, glide_client: TGlideClient): + key = get_random_string(10) + set_key = get_random_string(10) + + assert await glide_client.setbit(key, 0, 1) == 0 + assert await glide_client.setbit(key, 0, 0) == 1 + + # invalid argument - offset can't be negative + with pytest.raises(RequestError): + assert await glide_client.setbit(key, -1, 0) == 1 + + # key exists, but it is not a string + assert await glide_client.sadd(set_key, ["foo"]) == 1 + with pytest.raises(RequestError): + await glide_client.setbit(set_key, 0, 0) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_getbit(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + set_key = get_random_string(10) + value = "foobar" + + assert await glide_client.set(key, value) == OK + assert await glide_client.getbit(key, 1) == 1 + # When offset is beyond the string length, the string is assumed to be a contiguous space with 0 bits. + assert await glide_client.getbit(key, 1000) == 0 + # When key does not exist it is assumed to be an empty string, so offset is always out of range and the value is + # also assumed to be a contiguous space with 0 bits. + assert await glide_client.getbit(non_existing_key, 1) == 0 + + # invalid argument - offset can't be negative + with pytest.raises(RequestError): + assert await glide_client.getbit(key, -1) == 1 + + # key exists, but it is not a string + assert await glide_client.sadd(set_key, ["foo"]) == 1 + with pytest.raises(RequestError): + await glide_client.getbit(set_key, 0) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitpos_and_bitpos_interval(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + set_key = get_random_string(10) + value = ( + "?f0obar" # 00111111 01100110 00110000 01101111 01100010 01100001 01110010 + ) + + assert await glide_client.set(key, value) == OK + assert await glide_client.bitpos(key, 0) == 0 + assert await glide_client.bitpos(key, 1) == 2 + assert await glide_client.bitpos(key, 1, 1) == 9 + assert await glide_client.bitpos_interval(key, 0, 3, 5) == 24 + + # `BITPOS` returns -1 for non-existing strings + assert await glide_client.bitpos(non_existing_key, 1) == -1 + assert await glide_client.bitpos_interval(non_existing_key, 1, 3, 5) == -1 + + # invalid argument - bit value must be 0 or 1 + with pytest.raises(RequestError): + await glide_client.bitpos(key, 2) + with pytest.raises(RequestError): + await glide_client.bitpos_interval(key, 2, 3, 5) + + # key exists, but it is not a string + assert await glide_client.sadd(set_key, [value]) == 1 + with pytest.raises(RequestError): + await glide_client.bitpos(set_key, 1) + with pytest.raises(RequestError): + await glide_client.bitpos_interval(set_key, 1, 1, -1) + + if await check_if_server_version_lt(glide_client, "7.0.0"): + # error thrown because BIT and BYTE options were implemented after 7.0.0 + with pytest.raises(RequestError): + await glide_client.bitpos_interval(key, 1, 1, -1, BitmapIndexType.BYTE) + with pytest.raises(RequestError): + await glide_client.bitpos_interval(key, 1, 1, -1, BitmapIndexType.BIT) + else: + assert ( + await glide_client.bitpos_interval(key, 0, 3, 5, BitmapIndexType.BYTE) + == 24 + ) + assert ( + await glide_client.bitpos_interval(key, 1, 43, -2, BitmapIndexType.BIT) + == 47 + ) + assert ( + await glide_client.bitpos_interval( + non_existing_key, 1, 3, 5, BitmapIndexType.BYTE + ) + == -1 + ) + assert ( + await glide_client.bitpos_interval( + non_existing_key, 1, 3, 5, BitmapIndexType.BIT + ) + == -1 + ) + + # key exists, but it is not a string + with pytest.raises(RequestError): + await glide_client.bitpos_interval( + set_key, 1, 1, -1, BitmapIndexType.BIT + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitop(self, glide_client: TGlideClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + keys: List[TEncodable] = [key1, key2] + destination: TEncodable = f"{{testKey}}:3-{get_random_string(10)}" + non_existing_key1 = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key2 = f"{{testKey}}:5-{get_random_string(10)}" + non_existing_keys: List[TEncodable] = [non_existing_key1, non_existing_key2] + set_key = f"{{testKey}}:6-{get_random_string(10)}" + value1 = "foobar" + value2 = "abcdef" + + assert await glide_client.set(key1, value1) == OK + assert await glide_client.set(key2, value2) == OK + assert await glide_client.bitop(BitwiseOperation.AND, destination, keys) == 6 + assert await glide_client.get(destination) == b"`bc`ab" + assert await glide_client.bitop(BitwiseOperation.OR, destination, keys) == 6 + assert await glide_client.get(destination) == b"goofev" + + # reset values for simplicity of results in XOR + assert await glide_client.set(key1, "a") == OK + assert await glide_client.set(key2, "b") == OK + assert await glide_client.bitop(BitwiseOperation.XOR, destination, keys) == 1 + assert await glide_client.get(destination) == "\u0003".encode() + + # test single source key + assert await glide_client.bitop(BitwiseOperation.AND, destination, [key1]) == 1 + assert await glide_client.get(destination) == b"a" + assert await glide_client.bitop(BitwiseOperation.OR, destination, [key1]) == 1 + assert await glide_client.get(destination) == b"a" + assert await glide_client.bitop(BitwiseOperation.XOR, destination, [key1]) == 1 + assert await glide_client.get(destination) == b"a" + assert await glide_client.bitop(BitwiseOperation.NOT, destination, [key1]) == 1 + # currently, attempting to get the value from destination after the above NOT incorrectly raises an error + # TODO: update with a GET call once fix is implemented for https://github.com/valkey-io/valkey-glide/issues/1447 + + assert await glide_client.setbit(key1, 0, 1) == 0 + assert await glide_client.bitop(BitwiseOperation.NOT, destination, [key1]) == 1 + assert await glide_client.get(destination) == "\u001e".encode() + + # stores None when all keys hold empty strings + assert ( + await glide_client.bitop( + BitwiseOperation.AND, destination, non_existing_keys + ) + == 0 + ) + assert await glide_client.get(destination) is None + assert ( + await glide_client.bitop( + BitwiseOperation.OR, destination, non_existing_keys + ) + == 0 + ) + assert await glide_client.get(destination) is None + assert ( + await glide_client.bitop( + BitwiseOperation.XOR, destination, non_existing_keys + ) + == 0 + ) + assert await glide_client.get(destination) is None + assert ( + await glide_client.bitop( + BitwiseOperation.NOT, destination, [non_existing_key1] + ) + == 0 + ) + assert await glide_client.get(destination) is None + + # invalid argument - source key list cannot be empty + with pytest.raises(RequestError): + await glide_client.bitop(BitwiseOperation.OR, destination, []) + + # invalid arguments - NOT cannot be passed more than 1 key + with pytest.raises(RequestError): + await glide_client.bitop(BitwiseOperation.NOT, destination, [key1, key2]) + + assert await glide_client.sadd(set_key, [value1]) == 1 + # invalid argument - source key has the wrong type + with pytest.raises(RequestError): + await glide_client.bitop(BitwiseOperation.AND, destination, [set_key]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitfield(self, glide_client: TGlideClient): + key1 = get_random_string(10) + key2 = get_random_string(10) + non_existing_key = get_random_string(10) + set_key = get_random_string(10) + foobar = "foobar" + u2 = UnsignedEncoding(2) + u7 = UnsignedEncoding(7) + i3 = SignedEncoding(3) + i8 = SignedEncoding(8) + offset1 = BitOffset(1) + offset5 = BitOffset(5) + offset_multiplier4 = BitOffsetMultiplier(4) + offset_multiplier8 = BitOffsetMultiplier(8) + overflow_set = BitFieldSet(u2, offset1, -10) + overflow_get = BitFieldGet(u2, offset1) + + # binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + assert await glide_client.set(key1, foobar) == OK + + # SET tests + assert await glide_client.bitfield( + key1, + [ + # binary value becomes: 0(10)00110 01101111 01101111 01100010 01100001 01110010 + BitFieldSet(u2, offset1, 2), + # binary value becomes: 01000(011) 01101111 01101111 01100010 01100001 01110010 + BitFieldSet(i3, offset5, 3), + # binary value becomes: 01000011 01101111 01101111 0110(0010 010)00001 01110010 + BitFieldSet(u7, offset_multiplier4, 18), + # addressing with SET or INCRBY bits outside the current string length will enlarge the string, + # zero-padding it, as needed, for the minimal length needed, according to the most far bit touched. + # + # binary value becomes: + # 01000011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 (00010100) + BitFieldSet(i8, offset_multiplier8, 20), + BitFieldGet(u2, offset1), + BitFieldGet(i3, offset5), + BitFieldGet(u7, offset_multiplier4), + BitFieldGet(i8, offset_multiplier8), + ], + ) == [3, -2, 19, 0, 2, 3, 18, 20] + + # INCRBY tests + assert await glide_client.bitfield( + key1, + [ + # binary value becomes: + # 0(11)00011 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 + BitFieldIncrBy(u2, offset1, 1), + # binary value becomes: + # 01100(101) 01101111 01101111 01100010 01000001 01110010 00000000 00000000 00010100 + BitFieldIncrBy(i3, offset5, 2), + # binary value becomes: + # 01100101 01101111 01101111 0110(0001 111)00001 01110010 00000000 00000000 00010100 + BitFieldIncrBy(u7, offset_multiplier4, -3), + # binary value becomes: + # 01100101 01101111 01101111 01100001 11100001 01110010 00000000 00000000 (00011110) + BitFieldIncrBy(i8, offset_multiplier8, 10), + ], + ) == [3, -3, 15, 30] + + # OVERFLOW WRAP is used by default if no OVERFLOW is specified + assert await glide_client.bitfield( + key2, + [ + overflow_set, + BitFieldOverflow(BitOverflowControl.WRAP), + overflow_set, + overflow_get, + ], + ) == [0, 2, 2] + + # OVERFLOW affects only SET or INCRBY after OVERFLOW subcommand + assert await glide_client.bitfield( + key2, + [ + overflow_set, + BitFieldOverflow(BitOverflowControl.SAT), + overflow_set, + overflow_get, + BitFieldOverflow(BitOverflowControl.FAIL), + overflow_set, + ], + ) == [2, 2, 3, None] + + # if the key doesn't exist, the operation is performed as though the missing value was a string with all bits + # set to 0. + assert await glide_client.bitfield( + non_existing_key, [BitFieldSet(UnsignedEncoding(2), BitOffset(3), 2)] + ) == [0] + + # empty subcommands argument returns an empty list + assert await glide_client.bitfield(key1, []) == [] + + # invalid argument - offset must be >= 0 + with pytest.raises(RequestError): + await glide_client.bitfield( + key1, [BitFieldSet(UnsignedEncoding(5), BitOffset(-1), 1)] + ) + + # invalid argument - encoding size must be > 0 + with pytest.raises(RequestError): + await glide_client.bitfield( + key1, [BitFieldSet(UnsignedEncoding(0), BitOffset(1), 1)] + ) + + # invalid argument - unsigned encoding size must be < 64 + with pytest.raises(RequestError): + await glide_client.bitfield( + key1, [BitFieldSet(UnsignedEncoding(64), BitOffset(1), 1)] + ) + + # invalid argument - signed encoding size must be < 65 + with pytest.raises(RequestError): + await glide_client.bitfield( + key1, [BitFieldSet(SignedEncoding(65), BitOffset(1), 1)] + ) + + # key exists, but it is not a string + assert await glide_client.sadd(set_key, [foobar]) == 1 + with pytest.raises(RequestError): + await glide_client.bitfield( + set_key, [BitFieldSet(SignedEncoding(3), BitOffset(1), 2)] + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_bitfield_read_only(self, glide_client: TGlideClient): + min_version = "6.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key = get_random_string(10) + non_existing_key = get_random_string(10) + set_key = get_random_string(10) + foobar = "foobar" + unsigned_offset_get = BitFieldGet(UnsignedEncoding(2), BitOffset(1)) + + # binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + assert await glide_client.set(key, foobar) == OK + assert await glide_client.bitfield_read_only( + key, + [ + # Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 + unsigned_offset_get, + # Get value in: 01100(110) 01101111 01101111 01100010 01100001 01110010 00010100 + BitFieldGet(SignedEncoding(3), BitOffset(5)), + # Get value in: 01100110 01101111 01101(111 0110)0010 01100001 01110010 00010100 + BitFieldGet(UnsignedEncoding(7), BitOffsetMultiplier(3)), + # Get value in: 01100110 01101111 (01101111) 01100010 01100001 01110010 00010100 + BitFieldGet(SignedEncoding(8), BitOffsetMultiplier(2)), + ], + ) == [3, -2, 118, 111] + # offset is greater than current length of string: the operation is performed like the missing part all consists + # of bits set to 0. + assert await glide_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(3), BitOffset(100))] + ) == [0] + # similarly, if the key doesn't exist, the operation is performed as though the missing value was a string with + # all bits set to 0. + assert await glide_client.bitfield_read_only( + non_existing_key, [unsigned_offset_get] + ) == [0] + + # empty subcommands argument returns an empty list + assert await glide_client.bitfield_read_only(key, []) == [] + + # invalid argument - offset must be >= 0 + with pytest.raises(RequestError): + await glide_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(5), BitOffset(-1))] + ) + + # invalid argument - encoding size must be > 0 + with pytest.raises(RequestError): + await glide_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(0), BitOffset(1))] + ) + + # invalid argument - unsigned encoding size must be < 64 + with pytest.raises(RequestError): + await glide_client.bitfield_read_only( + key, [BitFieldGet(UnsignedEncoding(64), BitOffset(1))] + ) + + # invalid argument - signed encoding size must be < 65 + with pytest.raises(RequestError): + await glide_client.bitfield_read_only( + key, [BitFieldGet(SignedEncoding(65), BitOffset(1))] + ) + + # key exists, but it is not a string + assert await glide_client.sadd(set_key, [foobar]) == 1 + with pytest.raises(RequestError): + await glide_client.bitfield_read_only(set_key, [unsigned_offset_get]) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_object_encoding(self, glide_client: TGlideClient): + string_key = get_random_string(10) + list_key = get_random_string(10) + hashtable_key = get_random_string(10) + intset_key = get_random_string(10) + set_listpack_key = get_random_string(10) + hash_hashtable_key = get_random_string(10) + hash_listpack_key = get_random_string(10) + skiplist_key = get_random_string(10) + zset_listpack_key = get_random_string(10) + stream_key = get_random_string(10) + non_existing_key = get_random_string(10) + + assert await glide_client.object_encoding(non_existing_key) is None + + assert await glide_client.set( + string_key, "a really loooooooooooooooooooooooooooooooooooooooong value" + ) + assert await glide_client.object_encoding(string_key) == "raw".encode() + + assert await glide_client.set(string_key, "2") == OK + assert await glide_client.object_encoding(string_key) == "int".encode() + + assert await glide_client.set(string_key, "value") == OK + assert await glide_client.object_encoding(string_key) == "embstr".encode() + + assert await glide_client.lpush(list_key, ["1"]) == 1 + if await check_if_server_version_lt(glide_client, "7.2.0"): + assert await glide_client.object_encoding(list_key) == "quicklist".encode() + else: + assert await glide_client.object_encoding(list_key) == "listpack".encode() + + # The default value of set-max-intset-entries is 512 + for i in range(0, 513): + assert await glide_client.sadd(hashtable_key, [str(i)]) == 1 + assert await glide_client.object_encoding(hashtable_key) == "hashtable".encode() + + assert await glide_client.sadd(intset_key, ["1"]) == 1 + assert await glide_client.object_encoding(intset_key) == "intset".encode() + + assert await glide_client.sadd(set_listpack_key, ["foo"]) == 1 + if await check_if_server_version_lt(glide_client, "7.2.0"): + assert ( + await glide_client.object_encoding(set_listpack_key) + == "hashtable".encode() + ) + else: + assert ( + await glide_client.object_encoding(set_listpack_key) + == "listpack".encode() + ) + + # The default value of hash-max-listpack-entries is 512 + for i in range(0, 513): + assert await glide_client.hset(hash_hashtable_key, {str(i): "2"}) == 1 + assert ( + await glide_client.object_encoding(hash_hashtable_key) + == "hashtable".encode() + ) + + assert await glide_client.hset(hash_listpack_key, {"1": "2"}) == 1 + if await check_if_server_version_lt(glide_client, "7.0.0"): + assert ( + await glide_client.object_encoding(hash_listpack_key) + == "ziplist".encode() + ) + else: + assert ( + await glide_client.object_encoding(hash_listpack_key) + == "listpack".encode() + ) + + # The default value of zset-max-listpack-entries is 128 + for i in range(0, 129): + assert await glide_client.zadd(skiplist_key, {str(i): 2.0}) == 1 + assert await glide_client.object_encoding(skiplist_key) == "skiplist".encode() + + assert await glide_client.zadd(zset_listpack_key, {"1": 2.0}) == 1 + if await check_if_server_version_lt(glide_client, "7.0.0"): + assert ( + await glide_client.object_encoding(zset_listpack_key) + == "ziplist".encode() + ) + else: + assert ( + await glide_client.object_encoding(zset_listpack_key) + == "listpack".encode() + ) + + assert await glide_client.xadd(stream_key, [("field", "value")]) is not None + assert await glide_client.object_encoding(stream_key) == "stream".encode() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_object_freq(self, glide_client: TGlideClient): + key = get_random_string(10) + non_existing_key = get_random_string(10) + maxmemory_policy_key = "maxmemory-policy" + config = await glide_client.config_get([maxmemory_policy_key]) + config_decoded = cast(dict, convert_bytes_to_string_object(config)) + assert config_decoded is not None + maxmemory_policy = cast(str, config_decoded.get(maxmemory_policy_key)) + + try: + assert ( + await glide_client.config_set({maxmemory_policy_key: "allkeys-lfu"}) + == OK + ) + assert await glide_client.object_freq(non_existing_key) is None + assert await glide_client.set(key, "") == OK + freq = await glide_client.object_freq(key) + assert freq is not None and freq >= 0 + finally: + await glide_client.config_set({maxmemory_policy_key: maxmemory_policy}) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_object_idletime(self, glide_client: TGlideClient): + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + + assert await glide_client.object_idletime(non_existing_key) is None + assert await glide_client.set(string_key, "foo") == OK + time.sleep(2) + idletime = await glide_client.object_idletime(string_key) + assert idletime is not None and idletime > 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_object_refcount(self, glide_client: TGlideClient): + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + + assert await glide_client.object_refcount(non_existing_key) is None + assert await glide_client.set(string_key, "foo") == OK + refcount = await glide_client.object_refcount(string_key) + assert refcount is not None and refcount >= 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_load(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # verify function does not yet exist + assert await glide_client.function_list(lib_name) == [] + + assert await glide_client.function_load(code) == lib_name.encode() + + assert await glide_client.fcall(func_name, arguments=["one", "two"]) == b"one" + assert ( + await glide_client.fcall_ro(func_name, arguments=["one", "two"]) == b"one" + ) + + # verify with FUNCTION LIST + check_function_list_response( + await glide_client.function_list(lib_name, with_code=True), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + + # re-load library without replace + with pytest.raises(RequestError) as e: + await glide_client.function_load(code) + assert "Library '" + lib_name + "' already exists" in str(e) + + # re-load library with replace + assert await glide_client.function_load(code, True) == lib_name.encode() + + func2_name = f"myfunc2c{get_random_string(5)}" + new_code = f"""{code}\n redis.register_function({func2_name}, function(keys, args) return #args end)""" + new_code = generate_lua_lib_code( + lib_name, {func_name: "return args[1]", func2_name: "return #args"}, True + ) + + assert await glide_client.function_load(new_code, True) == lib_name.encode() + + assert await glide_client.fcall(func2_name, arguments=["one", "two"]) == 2 + assert await glide_client.fcall_ro(func2_name, arguments=["one", "two"]) == 2 + + assert await glide_client.function_flush(FlushMode.SYNC) is OK + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.parametrize("single_route", [True, False]) + async def test_function_load_cluster_with_route( + self, glide_client: GlideClusterClient, single_route: bool + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + route = SlotKeyRoute(SlotType.PRIMARY, "1") if single_route else AllPrimaries() + + # verify function does not yet exist + function_list = await glide_client.function_list(lib_name, False, route) + if single_route: + assert function_list == [] + else: + assert isinstance(function_list, dict) + for functions in function_list.values(): + assert functions == [] + + assert await glide_client.function_load(code, False, route) == lib_name.encode() + + result = await glide_client.fcall_route( + func_name, arguments=["one", "two"], route=route + ) + + if single_route: + assert result == b"one" + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert nodeResponse == b"one" + + result = await glide_client.fcall_ro_route( + func_name, arguments=["one", "two"], route=route + ) + + if single_route: + assert result == b"one" + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert nodeResponse == b"one" + + # verify with FUNCTION LIST + function_list = await glide_client.function_list( + lib_name, with_code=True, route=route + ) + if single_route: + check_function_list_response( + function_list, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + else: + assert isinstance(function_list, dict) + for nodeResponse in function_list.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + + # re-load library without replace + with pytest.raises(RequestError) as e: + await glide_client.function_load(code, False, route) + assert "Library '" + lib_name + "' already exists" in str(e) + + # re-load library with replace + assert await glide_client.function_load(code, True, route) == lib_name.encode() + + func2_name = f"myfunc2c{get_random_string(5)}" + new_code = f"""{code}\n redis.register_function({func2_name}, function(keys, args) return #args end)""" + new_code = generate_lua_lib_code( + lib_name, {func_name: "return args[1]", func2_name: "return #args"}, True + ) + + assert ( + await glide_client.function_load(new_code, True, route) == lib_name.encode() + ) + + result = await glide_client.fcall_route( + func2_name, arguments=["one", "two"], route=route + ) + + if single_route: + assert result == 2 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert nodeResponse == 2 + + result = await glide_client.fcall_ro_route( + func2_name, arguments=["one", "two"], route=route + ) + + if single_route: + assert result == 2 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert nodeResponse == 2 + + assert await glide_client.function_flush(FlushMode.SYNC, route) is OK + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_list(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + original_functions_count = len(await glide_client.function_list()) + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # Assert function `lib_name` does not yet exist + assert await glide_client.function_list(lib_name) == [] + + # load library + await glide_client.function_load(code) + + check_function_list_response( + await glide_client.function_list(lib_name.encode()), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + check_function_list_response( + await glide_client.function_list(f"{lib_name}*"), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + check_function_list_response( + await glide_client.function_list(lib_name, with_code=True), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + + no_args_response = await glide_client.function_list() + wildcard_pattern_response = await glide_client.function_list( + "*".encode(), False + ) + assert len(no_args_response) == original_functions_count + 1 + assert len(wildcard_pattern_response) == original_functions_count + 1 + check_function_list_response( + no_args_response, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + check_function_list_response( + wildcard_pattern_response, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.parametrize("single_route", [True, False]) + async def test_function_list_with_routing( + self, glide_client: GlideClusterClient, single_route: bool + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + route = SlotKeyRoute(SlotType.PRIMARY, "1") if single_route else AllPrimaries() + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # Assert function `lib_name` does not yet exist + result = await glide_client.function_list(lib_name, route=route) + if single_route: + assert result == [] + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert nodeResponse == [] + + # load library + await glide_client.function_load(code, route=route) + + result = await glide_client.function_list(lib_name, route=route) + if single_route: + check_function_list_response( + result, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + + result = await glide_client.function_list(f"{lib_name}*", route=route) + if single_route: + check_function_list_response( + result, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + + result = await glide_client.function_list(lib_name, with_code=True, route=route) + if single_route: + check_function_list_response( + result, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_list_with_multiple_functions( + self, glide_client: TGlideClient + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + await glide_client.function_flush() + assert len(await glide_client.function_list()) == 0 + + lib_name_1 = f"mylib1C{get_random_string(5)}" + func_name_1 = f"myfunc1c{get_random_string(5)}" + func_name_2 = f"myfunc2c{get_random_string(5)}" + code_1 = generate_lua_lib_code( + lib_name_1, + {func_name_1: "return args[1]", func_name_2: "return args[2]"}, + False, + ) + await glide_client.function_load(code_1) + + lib_name_2 = f"mylib2C{get_random_string(5)}" + func_name_3 = f"myfunc3c{get_random_string(5)}" + code_2 = generate_lua_lib_code( + lib_name_2, {func_name_3: "return args[3]"}, True + ) + await glide_client.function_load(code_2) + + no_args_response = await glide_client.function_list() + + assert len(no_args_response) == 2 + check_function_list_response( + no_args_response, + lib_name_1, + {func_name_1: None, func_name_2: None}, + {func_name_1: set(), func_name_2: set()}, + None, + ) + check_function_list_response( + no_args_response, + lib_name_2, + {func_name_3: None}, + {func_name_3: {b"no-writes"}}, + None, + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_flush(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + pytest.skip(f"Valkey version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # Load the function + assert await glide_client.function_load(code) == lib_name.encode() + + # verify function exists + assert len(await glide_client.function_list(lib_name)) == 1 + + # Flush functions + assert await glide_client.function_flush(FlushMode.SYNC) == OK + assert await glide_client.function_flush(FlushMode.ASYNC) == OK + + # verify function is removed + assert len(await glide_client.function_list(lib_name)) == 0 + + # Attempt to re-load library without overwriting to ensure FLUSH was effective + assert await glide_client.function_load(code) == lib_name.encode() + + # verify function exists + assert len(await glide_client.function_list(lib_name)) == 1 + + # Clean up by flushing functions again + await glide_client.function_flush() + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.parametrize("single_route", [True, False]) + async def test_function_flush_with_routing( + self, glide_client: GlideClusterClient, single_route: bool + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + pytest.skip(f"Valkey version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + route = SlotKeyRoute(SlotType.PRIMARY, "1") if single_route else AllPrimaries() + + # Load the function + assert await glide_client.function_load(code, False, route) == lib_name.encode() + + # verify function exists + result = await glide_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 1 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 1 + + # Flush functions + assert await glide_client.function_flush(FlushMode.SYNC, route) == OK + assert await glide_client.function_flush(FlushMode.ASYNC, route) == OK + + # verify function is removed + result = await glide_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 0 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 0 + + # Attempt to re-load library without overwriting to ensure FLUSH was effective + assert await glide_client.function_load(code, False, route) == lib_name.encode() + + # verify function exists + result = await glide_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 1 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 1 + + # Clean up by flushing functions again + assert await glide_client.function_flush(route=route) == OK + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_delete(self, glide_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + pytest.skip(f"Valkey version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # Load the function + assert await glide_client.function_load(code) == lib_name.encode() + + # verify function exists + assert len(await glide_client.function_list(lib_name)) == 1 + + # Delete the function + assert await glide_client.function_delete(lib_name) == OK + + # verify function is removed + assert len(await glide_client.function_list(lib_name)) == 0 + + # deleting a non-existing library + with pytest.raises(RequestError) as e: + await glide_client.function_delete(lib_name) + assert "Library not found" in str(e) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.parametrize("single_route", [True, False]) + async def test_function_delete_with_routing( + self, glide_client: GlideClusterClient, single_route: bool + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + pytest.skip(f"Valkey version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + route = SlotKeyRoute(SlotType.PRIMARY, "1") if single_route else AllPrimaries() + + # Load the function + assert await glide_client.function_load(code, False, route) == lib_name.encode() + + # verify function exists + result = await glide_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 1 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 1 + + # Delete the function + assert await glide_client.function_delete(lib_name, route) == OK + + # verify function is removed + result = await glide_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 0 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 0 + + # deleting a non-existing library + with pytest.raises(RequestError) as e: + await glide_client.function_delete(lib_name) + assert "Library not found" in str(e) + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_stats(self, glide_client: GlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + lib_name = "functionStats" + func_name = lib_name + assert await glide_client.function_flush(FlushMode.SYNC) == OK + + # function $funcName returns first argument + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False) + assert await glide_client.function_load(code, True) == lib_name.encode() + + response = await glide_client.function_stats() + check_function_stats_response(response, [], 1, 1) + + code = generate_lua_lib_code( + lib_name + "_2", + {func_name + "_2": "return 'OK'", func_name + "_3": "return 42"}, + False, + ) + assert ( + await glide_client.function_load(code, True) == (lib_name + "_2").encode() + ) + + response = await glide_client.function_stats() + check_function_stats_response(response, [], 2, 3) + + assert await glide_client.function_flush(FlushMode.SYNC) == OK + + response = await glide_client.function_stats() + check_function_stats_response(response, [], 0, 0) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_stats_cluster(self, glide_client: GlideClusterClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + lib_name = "functionStats_without_route" + func_name = lib_name + assert await glide_client.function_flush(FlushMode.SYNC) == OK + + # function $funcName returns first argument + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False) + assert await glide_client.function_load(code, True) == lib_name.encode() + + response = await glide_client.function_stats() + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsResponse, node_response), [], 1, 1 + ) + + code = generate_lua_lib_code( + lib_name + "_2", + {func_name + "_2": "return 'OK'", func_name + "_3": "return 42"}, + False, + ) + assert ( + await glide_client.function_load(code, True) == (lib_name + "_2").encode() + ) + + response = await glide_client.function_stats() + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsResponse, node_response), [], 2, 3 + ) + + assert await glide_client.function_flush(FlushMode.SYNC) == OK + + response = await glide_client.function_stats() + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsResponse, node_response), [], 0, 0 + ) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.parametrize("single_route", [True, False]) + async def test_function_stats_with_routing( + self, glide_client: GlideClusterClient, single_route: bool + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + route = ( + SlotKeyRoute(SlotType.PRIMARY, get_random_string(10)) + if single_route + else AllPrimaries() + ) + lib_name = "functionStats_with_route_" + str(single_route) + func_name = lib_name + assert await glide_client.function_flush(FlushMode.SYNC, route) == OK + + # function $funcName returns first argument + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, False) + assert await glide_client.function_load(code, True, route) == lib_name.encode() + + response = await glide_client.function_stats(route) + if single_route: + check_function_stats_response( + cast(TFunctionStatsResponse, response), [], 1, 1 + ) + else: + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsResponse, node_response), [], 1, 1 + ) + + code = generate_lua_lib_code( + lib_name + "_2", + {func_name + "_2": "return 'OK'", func_name + "_3": "return 42"}, + False, + ) + assert ( + await glide_client.function_load(code, True, route) + == (lib_name + "_2").encode() + ) + + response = await glide_client.function_stats(route) + if single_route: + check_function_stats_response( + cast(TFunctionStatsResponse, response), [], 2, 3 + ) + else: + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsResponse, node_response), [], 2, 3 + ) + + assert await glide_client.function_flush(FlushMode.SYNC, route) == OK + + response = await glide_client.function_stats(route) + if single_route: + check_function_stats_response( + cast(TFunctionStatsResponse, response), [], 0, 0 + ) + else: + for node_response in response.values(): + check_function_stats_response( + cast(TFunctionStatsResponse, node_response), [], 0, 0 + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_kill_no_write( + self, request, cluster_mode, protocol, glide_client: TGlideClient + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = create_lua_lib_with_long_running_function(lib_name, func_name, 10, True) + + # nothing to kill + with pytest.raises(RequestError) as e: + await glide_client.function_kill() + assert "NotBusy" in str(e) + + # load the library + assert await glide_client.function_load(code, replace=True) == lib_name.encode() + + # create a second client to run fcall + test_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=15000 + ) + + # call fcall to run the function + # make sure that fcall routes to a primary node, and not a replica + # if this happens, function_kill and function_stats won't find the function and will fail + primaryRoute = SlotKeyRoute(SlotType.PRIMARY, lib_name) + + async def endless_fcall_route_call(): + # fcall is supposed to be killed, and will return a RequestError + with pytest.raises(RequestError) as e: + if cluster_mode: + await test_client.fcall_ro_route( + func_name, arguments=[], route=primaryRoute + ) + else: + await test_client.fcall_ro(func_name, arguments=[]) + assert "Script killed by user" in str(e) + + async def wait_and_function_kill(): + # it can take a few seconds for FCALL to register as running + await asyncio.sleep(3) + timeout = 0 + while timeout <= 5: + # keep trying to kill until we get an "OK" + try: + result = await glide_client.function_kill() + # we expect to get success + assert result == "OK" + break + except RequestError: + # a RequestError may occur if the function is not yet running + # sleep and try again + timeout += 0.5 + await asyncio.sleep(0.5) + + await asyncio.gather( + endless_fcall_route_call(), + wait_and_function_kill(), + ) + + # no functions running so we get notbusy error again + with pytest.raises(RequestError) as e: + assert await glide_client.function_kill() + assert "NotBusy" in str(e) + + @pytest.mark.parametrize("cluster_mode", [False, True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_kill_write_is_unkillable( + self, request, cluster_mode, protocol, glide_client: TGlideClient + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = create_lua_lib_with_long_running_function(lib_name, func_name, 10, False) + + # load the library on all primaries + assert await glide_client.function_load(code, replace=True) == lib_name.encode() + + # create a second client to run fcall - and give it a long timeout + test_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=15000 + ) + + # call fcall to run the function loaded function + async def endless_fcall_route_call(): + # fcall won't be killed, because kill only works against fcalls that don't make a write operation + # use fcall(key) so that it makes a write operation + await test_client.fcall(func_name, keys=[lib_name]) + + async def wait_and_function_kill(): + # it can take a few seconds for FCALL to register as running + await asyncio.sleep(3) + timeout = 0 + foundUnkillable = False + while timeout <= 5: + # keep trying to kill until we get a unkillable return error + try: + await glide_client.function_kill() + except RequestError as e: + if "UNKILLABLE" in str(e): + foundUnkillable = True + break + timeout += 0.5 + await asyncio.sleep(0.5) + # expect an unkillable error + assert foundUnkillable + + await asyncio.gather( + endless_fcall_route_call(), + wait_and_function_kill(), + ) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_fcall_with_key(self, glide_client: GlideClusterClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + keys: List[TEncodable] = [key1, key2] + route = SlotKeyRoute(SlotType.PRIMARY, key1) + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return keys[1]"}, True) + + assert await glide_client.function_flush(FlushMode.SYNC, route) is OK + assert await glide_client.function_load(code, False, route) == lib_name.encode() + + assert ( + await glide_client.fcall(func_name, keys=keys, arguments=[]) + == key1.encode() + ) + + assert ( + await glide_client.fcall_ro(func_name, keys=keys, arguments=[]) + == key1.encode() + ) + + transaction = ClusterTransaction() + + transaction.fcall(func_name, keys=keys, arguments=[]) + transaction.fcall_ro(func_name, keys=keys, arguments=[]) + + # check response from a routed transaction request + result = await glide_client.exec(transaction, route) + assert result is not None + assert result[0] == key1.encode() + assert result[1] == key1.encode() + + # if no route given, GLIDE should detect it automatically + result = await glide_client.exec(transaction) + assert result is not None + assert result[0] == key1.encode() + assert result[1] == key1.encode() + + assert await glide_client.function_flush(FlushMode.SYNC, route) is OK + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_fcall_readonly_function(self, glide_client: GlideClusterClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + lib_name = f"fcall_readonly_function{get_random_string(5)}" + # intentionally using a REPLICA route + replicaRoute = SlotKeyRoute(SlotType.REPLICA, lib_name) + primaryRoute = SlotKeyRoute(SlotType.PRIMARY, lib_name) + func_name = f"fcall_readonly_function{get_random_string(5)}" + + # function $funcName returns a magic number + code = generate_lua_lib_code(lib_name, {func_name: "return 42"}, False) + + assert await glide_client.function_load(code, False) == lib_name.encode() + + # On a replica node should fail, because a function isn't guaranteed to be RO + with pytest.raises(RequestError) as e: + assert await glide_client.fcall_route( + func_name, arguments=[], route=replicaRoute + ) + assert "You can't write against a read only replica." in str(e) + + with pytest.raises(RequestError) as e: + assert await glide_client.fcall_ro_route( + func_name, arguments=[], route=replicaRoute + ) + assert "You can't write against a read only replica." in str(e) + + # fcall_ro also fails to run it even on primary - another error + with pytest.raises(RequestError) as e: + assert await glide_client.fcall_ro_route( + func_name, arguments=[], route=primaryRoute + ) + assert "Can not execute a script with write flag using *_ro command." in str(e) + + # create the same function, but with RO flag + code = generate_lua_lib_code(lib_name, {func_name: "return 42"}, True) + assert await glide_client.function_load(code, True) == lib_name.encode() + + # fcall should succeed now + assert ( + await glide_client.fcall_ro_route( + func_name, arguments=[], route=replicaRoute + ) + == 42 + ) + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_dump_restore_standalone(self, glide_client: GlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + assert await glide_client.function_flush(FlushMode.SYNC) is OK + + # Dump an empty lib + emptyDump = await glide_client.function_dump() + assert emptyDump is not None and len(emptyDump) > 0 + + name1 = f"Foster{get_random_string(5)}" + name2 = f"Dogster{get_random_string(5)}" + + # function name1 returns first argument; function name2 returns argument array len + code = generate_lua_lib_code( + name1, {name1: "return args[1]", name2: "return #args"}, False + ) + assert await glide_client.function_load(code, True) == name1.encode() + flist = await glide_client.function_list(with_code=True) + + dump = await glide_client.function_dump() + assert dump is not None + + # restore without cleaning the lib and/or overwrite option causes an error + with pytest.raises(RequestError) as e: + assert await glide_client.function_restore(dump) + assert "already exists" in str(e) + + # APPEND policy also fails for the same reason (name collision) + with pytest.raises(RequestError) as e: + assert await glide_client.function_restore( + dump, FunctionRestorePolicy.APPEND + ) + assert "already exists" in str(e) + + # REPLACE policy succeed + assert ( + await glide_client.function_restore(dump, FunctionRestorePolicy.REPLACE) + is OK + ) + + # but nothing changed - all code overwritten + assert await glide_client.function_list(with_code=True) == flist + + # create lib with another name, but with the same function names + assert await glide_client.function_flush(FlushMode.SYNC) is OK + code = generate_lua_lib_code( + name2, {name1: "return args[1]", name2: "return #args"}, False + ) + assert await glide_client.function_load(code, True) == name2.encode() + + # REPLACE policy now fails due to a name collision + with pytest.raises(RequestError) as e: + await glide_client.function_restore(dump, FunctionRestorePolicy.REPLACE) + assert "already exists" in str(e) + + # FLUSH policy succeeds, but deletes the second lib + assert ( + await glide_client.function_restore(dump, FunctionRestorePolicy.FLUSH) is OK + ) + assert await glide_client.function_list(with_code=True) == flist + + # call restored functions + assert ( + await glide_client.fcall(name1, arguments=["meow", "woem"]) + == "meow".encode() + ) + assert await glide_client.fcall(name2, arguments=["meow", "woem"]) == 2 + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_dump_restore_cluster( + self, glide_client: GlideClusterClient + ): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + assert await glide_client.function_flush(FlushMode.SYNC) is OK + + # Dump an empty lib + emptyDump = await glide_client.function_dump() + assert emptyDump is not None and len(emptyDump) > 0 + + name1 = f"Foster{get_random_string(5)}" + libname1 = f"FosterLib{get_random_string(5)}" + name2 = f"Dogster{get_random_string(5)}" + libname2 = f"DogsterLib{get_random_string(5)}" + + # function name1 returns first argument; function name2 returns argument array len + code = generate_lua_lib_code( + libname1, {name1: "return args[1]", name2: "return #args"}, True + ) + assert await glide_client.function_load(code, True) == libname1.encode() + flist = await glide_client.function_list(with_code=True) + dump = await glide_client.function_dump(RandomNode()) + assert dump is not None and isinstance(dump, bytes) + + # restore without cleaning the lib and/or overwrite option causes an error + with pytest.raises(RequestError) as e: + assert await glide_client.function_restore(dump) + assert "already exists" in str(e) + + # APPEND policy also fails for the same reason (name collision) + with pytest.raises(RequestError) as e: + assert await glide_client.function_restore( + dump, FunctionRestorePolicy.APPEND + ) + assert "already exists" in str(e) + + # REPLACE policy succeed + assert ( + await glide_client.function_restore( + dump, FunctionRestorePolicy.REPLACE, route=AllPrimaries() + ) + is OK + ) + + # but nothing changed - all code overwritten + restoredFunctionList = await glide_client.function_list(with_code=True) + assert restoredFunctionList is not None + assert isinstance(restoredFunctionList, List) and len(restoredFunctionList) == 1 + assert restoredFunctionList[0]["library_name".encode()] == libname1.encode() + + # Note that function ordering may differ across nodes so we can't do a deep equals + assert len(restoredFunctionList[0]["functions".encode()]) == 2 + + # create lib with another name, but with the same function names + assert await glide_client.function_flush(FlushMode.SYNC) is OK + code = generate_lua_lib_code( + libname2, {name1: "return args[1]", name2: "return #args"}, True + ) + assert await glide_client.function_load(code, True) == libname2.encode() + restoredFunctionList = await glide_client.function_list(with_code=True) + assert restoredFunctionList is not None + assert isinstance(restoredFunctionList, List) and len(restoredFunctionList) == 1 + assert restoredFunctionList[0]["library_name".encode()] == libname2.encode() + + # REPLACE policy now fails due to a name collision + with pytest.raises(RequestError) as e: + await glide_client.function_restore(dump, FunctionRestorePolicy.REPLACE) + assert "already exists" in str(e) + + # FLUSH policy succeeds, but deletes the second lib + assert ( + await glide_client.function_restore(dump, FunctionRestorePolicy.FLUSH) is OK + ) + restoredFunctionList = await glide_client.function_list(with_code=True) + assert restoredFunctionList is not None + assert isinstance(restoredFunctionList, List) and len(restoredFunctionList) == 1 + assert restoredFunctionList[0]["library_name".encode()] == libname1.encode() + + # Note that function ordering may differ across nodes so we can't do a deep equals + assert len(restoredFunctionList[0]["functions".encode()]) == 2 + + # call restored functions + assert ( + await glide_client.fcall_ro(name1, arguments=["meow", "woem"]) + == "meow".encode() + ) + assert await glide_client.fcall_ro(name2, arguments=["meow", "woem"]) == 2 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_srandmember(self, glide_client: TGlideClient): + key = get_random_string(10) + string_key = get_random_string(10) + elements: List[TEncodable] = ["one", "two"] + assert await glide_client.sadd(key, elements) == 2 + + member = await glide_client.srandmember(key) + # TODO: remove when function signature is fixed + assert isinstance(member, bytes) + assert member.decode() in elements + assert await glide_client.srandmember("non_existing_key") is None + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.srandmember(string_key) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_srandmember_count(self, glide_client: TGlideClient): + key = get_random_string(10) + string_key = get_random_string(10) + elements: List[TEncodable] = ["one", "two"] + assert await glide_client.sadd(key, elements) == 2 + + # unique values are expected as count is positive + members = await glide_client.srandmember_count(key, 4) + assert len(members) == 2 + assert set(members) == {b"one", b"two"} + + # duplicate values are expected as count is negative + members = await glide_client.srandmember_count(key, -4) + assert len(members) == 4 + for member in members: + # TODO: remove when function signature is fixed + assert isinstance(member, bytes) + assert member.decode() in elements + + # empty return values for non-existing or empty keys + assert await glide_client.srandmember_count(key, 0) == [] + assert await glide_client.srandmember_count("non_existing_key", 0) == [] + + # key exists, but it is not a set + assert await glide_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await glide_client.srandmember_count(string_key, 8) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_flushall(self, glide_client: TGlideClient): + min_version = "6.2.0" + key = f"{{key}}-1{get_random_string(5)}" + value = get_random_string(5) + + await glide_client.set(key, value) + assert await glide_client.dbsize() > 0 + assert await glide_client.flushall() == OK + assert await glide_client.flushall(FlushMode.ASYNC) == OK + if not await check_if_server_version_lt(glide_client, min_version): + assert await glide_client.flushall(FlushMode.SYNC) == OK + assert await glide_client.dbsize() == 0 + + if isinstance(glide_client, GlideClusterClient): + await glide_client.set(key, value) + assert await glide_client.flushall(route=AllPrimaries()) == OK + assert await glide_client.flushall(FlushMode.ASYNC, AllPrimaries()) == OK + if not await check_if_server_version_lt(glide_client, min_version): + assert await glide_client.flushall(FlushMode.SYNC, AllPrimaries()) == OK + assert await glide_client.dbsize() == 0 + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_flushdb(self, glide_client: GlideClient): + min_version = "6.2.0" + key1 = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + value = get_random_string(5) + + # fill DB 0 and check size non-empty + assert await glide_client.select(0) == OK + await glide_client.set(key1, value) + assert await glide_client.dbsize() > 0 + + # fill DB 1 and check size non-empty + assert await glide_client.select(1) == OK + await glide_client.set(key2, value) + assert await glide_client.dbsize() > 0 + + # flush DB 1 and check again + assert await glide_client.flushdb() == OK + assert await glide_client.dbsize() == 0 + + # swith to DB 0, flush, and check + assert await glide_client.select(0) == OK + assert await glide_client.dbsize() > 0 + assert await glide_client.flushdb(FlushMode.ASYNC) == OK + assert await glide_client.dbsize() == 0 + + # verify flush SYNC + if not await check_if_server_version_lt(glide_client, min_version): + await glide_client.set(key2, value) + assert await glide_client.dbsize() > 0 + assert await glide_client.flushdb(FlushMode.SYNC) == OK + assert await glide_client.dbsize() == 0 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_getex(self, glide_client: TGlideClient): + min_version = "6.2.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + key1 = get_random_string(10) + non_existing_key = get_random_string(10) + value = get_random_string(10) + value_encoded = value.encode() + + assert await glide_client.set(key1, value) == OK + assert await glide_client.getex(non_existing_key) is None + assert await glide_client.getex(key1) == value_encoded + assert await glide_client.ttl(key1) == -1 + + # setting expiration timer + assert ( + await glide_client.getex(key1, ExpiryGetEx(ExpiryTypeGetEx.MILLSEC, 50)) + == value_encoded + ) + assert await glide_client.ttl(key1) != -1 + + # setting and clearing expiration timer + assert await glide_client.set(key1, value) == OK + assert ( + await glide_client.getex(key1, ExpiryGetEx(ExpiryTypeGetEx.SEC, 10)) + == value_encoded + ) + assert ( + await glide_client.getex(key1, ExpiryGetEx(ExpiryTypeGetEx.PERSIST, None)) + == value_encoded + ) + assert await glide_client.ttl(key1) == -1 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_copy_no_database(self, glide_client: TGlideClient): + min_version = "6.2.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + source = f"{{testKey}}:1-{get_random_string(10)}" + destination = f"{{testKey}}:2-{get_random_string(10)}" + value1 = get_random_string(5) + value2 = get_random_string(5) + value1_encoded = value1.encode() + + # neither key exists + assert await glide_client.copy(source, destination, replace=False) is False + assert await glide_client.copy(source, destination) is False + + # source exists, destination does not + await glide_client.set(source, value1) + assert await glide_client.copy(source, destination, replace=False) is True + assert await glide_client.get(destination) == value1_encoded + + # new value for source key + await glide_client.set(source, value2) + + # both exists, no REPLACE + assert await glide_client.copy(source, destination) is False + assert await glide_client.copy(source, destination, replace=False) is False + assert await glide_client.get(destination) == value1_encoded + + # both exists, with REPLACE + assert await glide_client.copy(source, destination, replace=True) is True + assert await glide_client.get(destination) == value2.encode() + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_copy_database(self, glide_client: GlideClient): + min_version = "6.2.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + source = get_random_string(10) + destination = get_random_string(10) + value1 = get_random_string(5) + value2 = get_random_string(5) + value1_encoded = value1.encode() + value2_encoded = value2.encode() + index0 = 0 + index1 = 1 + index2 = 2 + + try: + assert await glide_client.select(index0) == OK + + # neither key exists + assert ( + await glide_client.copy(source, destination, index1, replace=False) + is False + ) + + # source exists, destination does not + await glide_client.set(source, value1) + assert ( + await glide_client.copy(source, destination, index1, replace=False) + is True + ) + assert await glide_client.select(index1) == OK + assert await glide_client.get(destination) == value1_encoded + + # new value for source key + assert await glide_client.select(index0) == OK + await glide_client.set(source, value2) + + # no REPLACE, copying to existing key on DB 0 & 1, non-existing key on DB 2 + assert ( + await glide_client.copy(source, destination, index1, replace=False) + is False + ) + assert ( + await glide_client.copy(source, destination, index2, replace=False) + is True + ) + + # new value only gets copied to DB 2 + assert await glide_client.select(index1) == OK + assert await glide_client.get(destination) == value1_encoded + assert await glide_client.select(index2) == OK + assert await glide_client.get(destination) == value2_encoded + + # both exists, with REPLACE, when value isn't the same, source always get copied to destination + assert await glide_client.select(index0) == OK + assert ( + await glide_client.copy(source, destination, index1, replace=True) + is True + ) + assert await glide_client.select(index1) == OK + assert await glide_client.get(destination) == value2_encoded + + # invalid DB index + with pytest.raises(RequestError): + await glide_client.copy(source, destination, -1, replace=True) + finally: + assert await glide_client.select(0) == OK + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_wait(self, glide_client: TGlideClient): + key = f"{{key}}-1{get_random_string(5)}" + value = get_random_string(5) + value2 = get_random_string(5) + + assert await glide_client.set(key, value) == OK + if isinstance(glide_client, GlideClusterClient): + assert await glide_client.wait(1, 1000) >= 1 + else: + assert await glide_client.wait(1, 1000) >= 0 + + # ensure that command doesn't time out even if timeout > request timeout (250ms by default) + assert await glide_client.set(key, value2) == OK + assert await glide_client.wait(100, 500) >= 0 + + # command should fail on a negative timeout value + with pytest.raises(RequestError): + await glide_client.wait(1, -1) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lolwut(self, glide_client: TGlideClient): + result = await glide_client.lolwut() + assert b"Redis ver. " in result + result = await glide_client.lolwut(parameters=[]) + assert b"Redis ver. " in result + result = await glide_client.lolwut(parameters=[50, 20]) + assert b"Redis ver. " in result + result = await glide_client.lolwut(6) + assert b"Redis ver. " in result + result = await glide_client.lolwut(5, [30, 4, 4]) + assert b"Redis ver. " in result + + if isinstance(glide_client, GlideClusterClient): + # test with multi-node route + result = await glide_client.lolwut(route=AllNodes()) + assert isinstance(result, dict) + result_decoded = cast(dict, convert_bytes_to_string_object(result)) + assert result_decoded is not None + for node_result in result_decoded.values(): + assert "Redis ver. " in node_result + + result = await glide_client.lolwut(parameters=[10, 20], route=AllNodes()) + assert isinstance(result, dict) + result_decoded = cast(dict, convert_bytes_to_string_object(result)) + assert result_decoded is not None + for node_result in result_decoded.values(): + assert "Redis ver. " in node_result + + # test with single-node route + result = await glide_client.lolwut(2, route=RandomNode()) + assert isinstance(result, bytes) + assert b"Redis ver. " in result + + result = await glide_client.lolwut(2, [10, 20], RandomNode()) + assert isinstance(result, bytes) + assert b"Redis ver. " in result + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_client_random_key(self, glide_client: GlideClusterClient): + key = get_random_string(10) + + # setup: delete all keys + assert await glide_client.flushall(FlushMode.SYNC) + + # no keys exists, so random_key returns None + assert await glide_client.random_key() is None + + assert await glide_client.set(key, "foo") == OK + # `key` should be the only existing key, so random_key should return `key` + assert await glide_client.random_key() == key.encode() + assert await glide_client.random_key(AllPrimaries()) == key.encode() + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_client_random_key(self, glide_client: GlideClient): + key = get_random_string(10) + + # setup: delete all keys in DB 0 and DB 1 + assert await glide_client.select(0) == OK + assert await glide_client.flushdb(FlushMode.SYNC) == OK + assert await glide_client.select(1) == OK + assert await glide_client.flushdb(FlushMode.SYNC) == OK + + # no keys exist so random_key returns None + assert await glide_client.random_key() is None + # set `key` in DB 1 + assert await glide_client.set(key, "foo") == OK + # `key` should be the only key in the database + assert await glide_client.random_key() == key.encode() + + # switch back to DB 0 + assert await glide_client.select(0) == OK + # DB 0 should still have no keys, so random_key should still return None + assert await glide_client.random_key() is None + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_dump_restore(self, glide_client: TGlideClient): + key1 = f"{{key}}-1{get_random_string(10)}" + key2 = f"{{key}}-2{get_random_string(10)}" + key3 = f"{{key}}-3{get_random_string(10)}" + nonExistingKey = f"{{key}}-4{get_random_string(10)}" + value = get_random_string(5) + + await glide_client.set(key1, value) + + # Dump an existing key + bytesData = await glide_client.dump(key1) + assert bytesData is not None + + # Dump non-existing key + assert await glide_client.dump(nonExistingKey) is None + + # Restore to a new key and verify its value + assert await glide_client.restore(key2, 0, bytesData) == OK + newValue = await glide_client.get(key2) + assert newValue == value.encode() + + # Restore to an existing key + with pytest.raises(RequestError) as e: + await glide_client.restore(key2, 0, bytesData) + assert "Target key name already exists" in str(e) + + # Restore using a value with checksum error + with pytest.raises(RequestError) as e: + await glide_client.restore(key3, 0, value.encode()) + assert "payload version or checksum are wrong" in str(e) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_dump_restore_options(self, glide_client: TGlideClient): + key1 = f"{{key}}-1{get_random_string(10)}" + key2 = f"{{key}}-2{get_random_string(10)}" + key3 = f"{{key}}-3{get_random_string(10)}" + value = get_random_string(5) + + await glide_client.set(key1, value) + + # Dump an existing key + bytesData = await glide_client.dump(key1) + assert bytesData is not None + + # Restore without option + assert await glide_client.restore(key2, 0, bytesData) == OK + + # Restore with REPLACE option + assert await glide_client.restore(key2, 0, bytesData, replace=True) == OK + + # Restore to an existing key holding different value with REPLACE option + assert await glide_client.sadd(key3, ["a"]) == 1 + assert await glide_client.restore(key3, 0, bytesData, replace=True) == OK + + # Restore with REPLACE, ABSTTL, and positive TTL + assert ( + await glide_client.restore(key2, 1000, bytesData, replace=True, absttl=True) + == OK + ) + + # Restore with REPLACE, ABSTTL, and negative TTL + with pytest.raises(RequestError) as e: + await glide_client.restore(key2, -10, bytesData, replace=True, absttl=True) + assert "Invalid TTL value" in str(e) + # Restore with REPLACE and positive idletime assert ( - await redis_client.zrange( - key, - RangeByLex( - start=InfBound.NEG_INF, - stop=InfBound.POS_INF, - limit=Limit(offset=1, count=2), - ), - ) - ) == ["b", "c"] + await glide_client.restore(key2, 0, bytesData, replace=True, idletime=10) + == OK + ) - assert await redis_client.zrange( - key, - RangeByLex( - start=LexBoundary("c", is_inclusive=False), stop=InfBound.NEG_INF - ), - reverse=True, - ) == ["b", "a"] + # Restore with REPLACE and negative idletime + with pytest.raises(RequestError) as e: + await glide_client.restore(key2, 0, bytesData, replace=True, idletime=-10) + assert "Invalid IDLETIME value" in str(e) + # Restore with REPLACE and positive frequency assert ( - await redis_client.zrange( - key, - RangeByLex( - start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) - ), - reverse=True, - ) - == [] - ) # stop is greater than start with reverse set to True + await glide_client.restore(key2, 0, bytesData, replace=True, frequency=10) + == OK + ) - assert ( - await redis_client.zrange( - key, - RangeByLex( - start=InfBound.POS_INF, stop=LexBoundary("c", is_inclusive=False) - ), - ) - == [] - ) # start is greater than stop + # Restore with REPLACE and negative frequency + with pytest.raises(RequestError) as e: + await glide_client.restore(key2, 0, bytesData, replace=True, frequency=-10) + assert "Invalid FREQ value" in str(e) - @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("cluster_mode", [False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zrange_different_types_of_keys(self, redis_client: TRedisClient): - key = get_random_string(10) - + async def test_lcs(self, glide_client: GlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + key1 = "testKey1" + value1 = "abcd" + key2 = "testKey2" + value2 = "axcd" + nonexistent_key = "nonexistent_key" + expected_subsequence = "acd" + expected_subsequence_with_nonexistent_key = "" + assert await glide_client.mset({key1: value1, key2: value2}) == OK + assert await glide_client.lcs(key1, key2) == expected_subsequence.encode() assert ( - await redis_client.zrange("non_existing_key", RangeByIndex(start=0, stop=1)) - == [] + await glide_client.lcs(key1, nonexistent_key) + == expected_subsequence_with_nonexistent_key.encode() ) + lcs_non_string_key = "lcs_non_string_key" + assert await glide_client.sadd(lcs_non_string_key, ["Hello", "world"]) == 2 + with pytest.raises(RequestError): + await glide_client.lcs(key1, lcs_non_string_key) + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lcs_len(self, glide_client: GlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + key1 = "testKey1" + value1 = "abcd" + key2 = "testKey2" + value2 = "axcd" + nonexistent_key = "nonexistent_key" + expected_subsequence_length = 3 + expected_subsequence_length_with_nonexistent_key = 0 + assert await glide_client.mset({key1: value1, key2: value2}) == OK + assert await glide_client.lcs_len(key1, key2) == expected_subsequence_length assert ( - await redis_client.zrange_withscores( - "non_existing_key", RangeByIndex(start=0, stop=-1) - ) - ) == {} + await glide_client.lcs_len(key1, nonexistent_key) + == expected_subsequence_length_with_nonexistent_key + ) + lcs_non_string_key = "lcs_non_string_key" + assert await glide_client.sadd(lcs_non_string_key, ["Hello", "world"]) == 2 + with pytest.raises(RequestError): + await glide_client.lcs_len(key1, lcs_non_string_key) - assert await redis_client.set(key, "value") == OK + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lcs_idx(self, glide_client: GlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + key1 = "testKey1" + value1 = "abcd1234" + key2 = "testKey2" + value2 = "bcdef1234" + nonexistent_key = "nonexistent_key" + expected_response_no_min_match_len_no_with_match_len = { + b"matches": [ + [ + [4, 7], + [5, 8], + ], + [ + [1, 3], + [0, 2], + ], + ], + b"len": 7, + } + expected_response_with_min_match_len_equals_four_no_with_match_len = { + b"matches": [ + [ + [4, 7], + [5, 8], + ], + ], + b"len": 7, + } + expected_response_no_min_match_len_with_match_len = { + b"matches": [ + [ + [4, 7], + [5, 8], + 4, + ], + [ + [1, 3], + [0, 2], + 3, + ], + ], + b"len": 7, + } + expected_response_with_min_match_len_equals_four_and_with_match_len = { + b"matches": [ + [ + [4, 7], + [5, 8], + 4, + ], + ], + b"len": 7, + } + expected_response_with_nonexistent_key = { + b"matches": [], + b"len": 0, + } + assert await glide_client.mset({key1: value1, key2: value2}) == OK + assert ( + await glide_client.lcs_idx(key1, key2) + == expected_response_no_min_match_len_no_with_match_len + ) + assert ( + await glide_client.lcs_idx(key1, key2, min_match_len=4) + == expected_response_with_min_match_len_equals_four_no_with_match_len + ) + assert ( + # negative min_match_len should have no affect on the output + await glide_client.lcs_idx(key1, key2, min_match_len=-3) + == expected_response_no_min_match_len_no_with_match_len + ) + assert ( + await glide_client.lcs_idx(key1, key2, with_match_len=True) + == expected_response_no_min_match_len_with_match_len + ) + assert ( + await glide_client.lcs_idx(key1, key2, min_match_len=4, with_match_len=True) + == expected_response_with_min_match_len_equals_four_and_with_match_len + ) + assert ( + await glide_client.lcs_idx(key1, nonexistent_key) + == expected_response_with_nonexistent_key + ) + lcs_non_string_key = "lcs_non_string_key" + assert await glide_client.sadd(lcs_non_string_key, ["Hello", "world"]) == 2 with pytest.raises(RequestError): - await redis_client.zrange(key, RangeByIndex(start=0, stop=1)) + await glide_client.lcs_idx(key1, lcs_non_string_key) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_watch(self, glide_client: GlideClient): + # watched key didn't change outside of transaction before transaction execution, transaction will execute + assert await glide_client.set("key1", "original_value") == OK + assert await glide_client.watch(["key1"]) == OK + transaction = Transaction() + transaction.set("key1", "transaction_value") + transaction.get("key1") + assert await glide_client.exec(transaction) is not None + + # watched key changed outside of transaction before transaction execution, transaction will not execute + assert await glide_client.set("key1", "original_value") == OK + assert await glide_client.watch(["key1"]) == OK + transaction = Transaction() + transaction.set("key1", "transaction_value") + assert await glide_client.set("key1", "standalone_value") == OK + transaction.get("key1") + assert await glide_client.exec(transaction) is None + + # empty list not supported with pytest.raises(RequestError): - await redis_client.zrange_withscores(key, RangeByIndex(start=0, stop=1)) + await glide_client.watch([]) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_zrank(self, redis_client: TRedisClient): - key = get_random_string(10) - members_scores = {"one": 1.5, "two": 2, "three": 3} - assert await redis_client.zadd(key, members_scores) == 3 - assert await redis_client.zrank(key, "one") == 0 - if not await check_if_server_version_lt(redis_client, "7.2.0"): - assert await redis_client.zrank_withscore(key, "one") == [0, 1.5] - assert await redis_client.zrank_withscore(key, "non_existing_field") is None - assert ( - await redis_client.zrank_withscore("non_existing_key", "field") is None - ) + async def test_unwatch(self, glide_client: GlideClient): + + # watched key unwatched before transaction execution even if changed + # outside of transaction, transaction will still execute + assert await glide_client.set("key1", "original_value") == OK + assert await glide_client.watch(["key1"]) == OK + transaction = Transaction() + transaction.set("key1", "transaction_value") + assert await glide_client.set("key1", "standalone_value") == OK + transaction.get("key1") + assert await glide_client.unwatch() == OK + result = await glide_client.exec(transaction) + assert result is not None + assert isinstance(result, list) + assert len(result) == 2 + assert result[0] == "OK" + assert result[1] == b"transaction_value" - assert await redis_client.zrank(key, "non_existing_field") is None - assert await redis_client.zrank("non_existing_key", "field") is None + # UNWATCH returns OK when there no watched keys + assert await glide_client.unwatch() == OK - assert await redis_client.set(key, "value") == OK - with pytest.raises(RequestError): - await redis_client.zrank(key, "one") + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_unwatch_with_route(self, glide_client: GlideClusterClient): + assert await glide_client.unwatch(RandomNode()) == OK @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_type(self, redis_client: TRedisClient): - key = get_random_string(10) - assert await redis_client.set(key, "value") == OK - assert (await redis_client.type(key)).lower() == "string" - assert await redis_client.delete([key]) == 1 + async def test_lpos(self, glide_client: TGlideClient): + min_version = "6.0.6" + if await check_if_server_version_lt(glide_client, min_version): + # TODO: change it to pytest fixture after we'll implement a sync client + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + key = f"{{key}}-1{get_random_string(5)}" + non_list_key = f"{{key}}-2{get_random_string(5)}" + mylist: List[TEncodable] = ["a", "a", "b", "c", "a", "b"] - assert await redis_client.lpush(key, ["value"]) == 1 - assert (await redis_client.type(key)).lower() == "list" - assert await redis_client.delete([key]) == 1 + # basic case + await glide_client.rpush(key, mylist) + assert await glide_client.lpos(key, "b") == 2 - assert await redis_client.sadd(key, ["value"]) == 1 - assert (await redis_client.type(key)).lower() == "set" - assert await redis_client.delete([key]) == 1 + # reverse traversal + assert await glide_client.lpos(key, "b", -2) == 2 - assert await redis_client.zadd(key, {"member": 1.0}) == 1 - assert (await redis_client.type(key)).lower() == "zset" - assert await redis_client.delete([key]) == 1 + # unlimited comparisons + assert await glide_client.lpos(key, "a", 1, None, 0) == 0 - assert await redis_client.hset(key, {"field": "value"}) == 1 - assert (await redis_client.type(key)).lower() == "hash" - assert await redis_client.delete([key]) == 1 + # limited comparisons + assert await glide_client.lpos(key, "c", 1, None, 2) is None - await redis_client.custom_command(["XADD", key, "*", "field", "value"]) - assert await redis_client.type(key) == "stream" - assert await redis_client.delete([key]) == 1 + # element does not exist + assert await glide_client.lpos(key, "non_existing") is None - assert (await redis_client.type(key)).lower() == "none" + # with count + assert await glide_client.lpos(key, "a", 1, 0, 0) == [0, 1, 4] - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_echo(self, redis_client: TRedisClient): - message = get_random_string(5) - assert await redis_client.echo(message) == message - if isinstance(redis_client, RedisClusterClient): - echo_dict = await redis_client.echo(message, AllNodes()) - assert isinstance(echo_dict, dict) - for value in echo_dict.values(): - assert value == message + # with count and rank + assert await glide_client.lpos(key, "a", -2, 0, 0) == [1, 0] - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_dbsize(self, redis_client: TRedisClient): - assert await redis_client.custom_command(["FLUSHALL"]) == OK + # key does not exist + assert await glide_client.lpos("non_existing", "non_existing") is None - assert await redis_client.dbsize() == 0 - key_value_pairs = [(get_random_string(10), "foo") for _ in range(10)] + # invalid rank value + with pytest.raises(RequestError): + await glide_client.lpos(key, "a", 0) - for key, value in key_value_pairs: - assert await redis_client.set(key, value) == OK - assert await redis_client.dbsize() == 10 + # invalid count + with pytest.raises(RequestError): + await glide_client.lpos(non_list_key, "a", None, -1) - if isinstance(redis_client, RedisClusterClient): - assert await redis_client.custom_command(["FLUSHALL"]) == OK - key = get_random_string(5) - assert await redis_client.set(key, value) == OK - assert await redis_client.dbsize(SlotKeyRoute(SlotType.PRIMARY, key)) == 1 - else: - assert await redis_client.select(1) == OK - assert await redis_client.dbsize() == 0 + # invalid max_len + with pytest.raises(RequestError): + await glide_client.lpos(non_list_key, "a", None, None, -1) - @pytest.mark.parametrize("cluster_mode", [True, False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_time(self, redis_client: TRedisClient): - current_time = int(time.time()) - 1 - result = await redis_client.time() - assert len(result) == 2 - assert isinstance(result, list) - assert isinstance(result[0], str) - assert isinstance(result[1], str) - assert int(result[0]) > current_time - assert 0 < int(result[1]) < 1000000 + # wrong data type + await glide_client.set(non_list_key, "non_list_value") + with pytest.raises(RequestError): + await glide_client.lpos(non_list_key, "a") - @pytest.mark.parametrize("cluster_mode", [True, False]) + +class TestMultiKeyCommandCrossSlot: + @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_append(self, redis_client: TRedisClient): - key, value = get_random_string(10), get_random_string(5) - assert await redis_client.append(key, value) == 5 + async def test_multi_key_command_returns_cross_slot_error( + self, glide_client: GlideClusterClient + ): + promises: List[Any] = [ + glide_client.blpop(["abc", "zxy", "lkn"], 0.1), + glide_client.brpop(["abc", "zxy", "lkn"], 0.1), + glide_client.rename("abc", "zxy"), + glide_client.zdiffstore("abc", ["zxy", "lkn"]), + glide_client.zdiff(["abc", "zxy", "lkn"]), + glide_client.zdiff_withscores(["abc", "zxy", "lkn"]), + glide_client.zrangestore("abc", "zxy", RangeByIndex(0, -1)), + glide_client.zinterstore( + "{xyz}", cast(Union[List[Union[TEncodable]]], ["{abc}", "{def}"]) + ), + glide_client.zunionstore( + "{xyz}", cast(Union[List[Union[TEncodable]]], ["{abc}", "{def}"]) + ), + glide_client.bzpopmin(["abc", "zxy", "lkn"], 0.5), + glide_client.bzpopmax(["abc", "zxy", "lkn"], 0.5), + glide_client.smove("abc", "def", "_"), + glide_client.sunionstore("abc", ["zxy", "lkn"]), + glide_client.sinter(["abc", "zxy", "lkn"]), + glide_client.sinterstore("abc", ["zxy", "lkn"]), + glide_client.sdiff(["abc", "zxy", "lkn"]), + glide_client.sdiffstore("abc", ["def", "ghi"]), + glide_client.renamenx("abc", "def"), + glide_client.pfcount(["def", "ghi"]), + glide_client.pfmerge("abc", ["def", "ghi"]), + glide_client.zinter(["def", "ghi"]), + glide_client.zinter_withscores( + cast(Union[List[TEncodable]], ["def", "ghi"]) + ), + glide_client.zunion(["def", "ghi"]), + glide_client.zunion_withscores(cast(List[TEncodable], ["def", "ghi"])), + glide_client.sort_store("abc", "zxy"), + glide_client.lmove("abc", "zxy", ListDirection.LEFT, ListDirection.LEFT), + glide_client.blmove( + "abc", "zxy", ListDirection.LEFT, ListDirection.LEFT, 1 + ), + glide_client.msetnx({"abc": "abc", "zxy": "zyx"}), + glide_client.sunion(["def", "ghi"]), + glide_client.bitop(BitwiseOperation.OR, "abc", ["zxy", "lkn"]), + glide_client.xread({"abc": "0-0", "zxy": "0-0"}), + ] + + if not await check_if_server_version_lt(glide_client, "6.2.0"): + promises.extend( + [ + glide_client.geosearchstore( + "abc", + "zxy", + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + ), + glide_client.copy("abc", "zxy", replace=True), + ] + ) - assert await redis_client.append(key, value) == 10 - assert await redis_client.get(key) == value * 2 + if not await check_if_server_version_lt(glide_client, "7.0.0"): + promises.extend( + [ + glide_client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), + glide_client.zintercard(["abc", "def"]), + glide_client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), + glide_client.sintercard(["def", "ghi"]), + glide_client.lmpop(["def", "ghi"], ListDirection.LEFT), + glide_client.blmpop(["def", "ghi"], ListDirection.LEFT, 1), + glide_client.lcs("abc", "def"), + glide_client.lcs_len("abc", "def"), + glide_client.lcs_idx("abc", "def"), + glide_client.fcall("func", ["abc", "zxy", "lkn"], []), + glide_client.fcall_ro("func", ["abc", "zxy", "lkn"], []), + ] + ) + + for promise in promises: + with pytest.raises(RequestError) as e: + await promise + assert "crossslot" in str(e).lower() + + # TODO bz*, zunion, sdiff and others - all rest multi-key commands except ones tested below + pass + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_multi_key_command_routed_to_multiple_nodes( + self, glide_client: GlideClusterClient + ): + await glide_client.exists(["abc", "zxy", "lkn"]) + await glide_client.unlink(["abc", "zxy", "lkn"]) + await glide_client.delete(["abc", "zxy", "lkn"]) + await glide_client.mget(["abc", "zxy", "lkn"]) + await glide_client.mset({"abc": "1", "zxy": "2", "lkn": "3"}) + await glide_client.touch(["abc", "zxy", "lkn"]) + await glide_client.watch(["abc", "zxy", "lkn"]) class TestCommandsUnitTests: @@ -1990,6 +9552,50 @@ def test_expiry_cmd_args(self): ) assert exp_unix_millisec_datetime.get_cmd_args() == ["PXAT", "1682639759342"] + def test_get_expiry_cmd_args(self): + exp_sec = ExpiryGetEx(ExpiryTypeGetEx.SEC, 5) + assert exp_sec.get_cmd_args() == ["EX", "5"] + + exp_sec_timedelta = ExpiryGetEx(ExpiryTypeGetEx.SEC, timedelta(seconds=5)) + assert exp_sec_timedelta.get_cmd_args() == ["EX", "5"] + + exp_millsec = ExpiryGetEx(ExpiryTypeGetEx.MILLSEC, 5) + assert exp_millsec.get_cmd_args() == ["PX", "5"] + + exp_millsec_timedelta = ExpiryGetEx( + ExpiryTypeGetEx.MILLSEC, timedelta(seconds=5) + ) + assert exp_millsec_timedelta.get_cmd_args() == ["PX", "5000"] + + exp_millsec_timedelta = ExpiryGetEx( + ExpiryTypeGetEx.MILLSEC, timedelta(seconds=5) + ) + assert exp_millsec_timedelta.get_cmd_args() == ["PX", "5000"] + + exp_unix_sec = ExpiryGetEx(ExpiryTypeGetEx.UNIX_SEC, 1682575739) + assert exp_unix_sec.get_cmd_args() == ["EXAT", "1682575739"] + + exp_unix_sec_datetime = ExpiryGetEx( + ExpiryTypeGetEx.UNIX_SEC, + datetime(2023, 4, 27, 23, 55, 59, 342380, timezone.utc), + ) + assert exp_unix_sec_datetime.get_cmd_args() == ["EXAT", "1682639759"] + + exp_unix_millisec = ExpiryGetEx(ExpiryTypeGetEx.UNIX_MILLSEC, 1682586559964) + assert exp_unix_millisec.get_cmd_args() == ["PXAT", "1682586559964"] + + exp_unix_millisec_datetime = ExpiryGetEx( + ExpiryTypeGetEx.UNIX_MILLSEC, + datetime(2023, 4, 27, 23, 55, 59, 342380, timezone.utc), + ) + assert exp_unix_millisec_datetime.get_cmd_args() == ["PXAT", "1682639759342"] + + exp_persist = ExpiryGetEx( + ExpiryTypeGetEx.PERSIST, + None, + ) + assert exp_persist.get_cmd_args() == ["PERSIST"] + def test_expiry_raises_on_value_error(self): with pytest.raises(ValueError): ExpirySet(ExpiryType.SEC, 5.5) @@ -2007,13 +9613,16 @@ def test_is_single_response(self): class TestClusterRoutes: async def cluster_route_custom_command_multi_nodes( self, - redis_client: RedisClusterClient, + glide_client: GlideClusterClient, route: Route, ): - cluster_nodes = await redis_client.custom_command(["CLUSTER", "NODES"]) + cluster_nodes = await glide_client.custom_command(["CLUSTER", "NODES"]) + assert isinstance(cluster_nodes, bytes) + cluster_nodes = cluster_nodes.decode() assert isinstance(cluster_nodes, (str, list)) cluster_nodes = get_first_result(cluster_nodes) num_of_nodes = len(cluster_nodes.splitlines()) + assert isinstance(cluster_nodes, (str, list)) expected_num_of_results = ( num_of_nodes if isinstance(route, AllNodes) @@ -2024,12 +9633,14 @@ async def cluster_route_custom_command_multi_nodes( cluster_nodes.count("slave") if isinstance(route, AllNodes) else 0 ) - all_results = await redis_client.custom_command(["INFO", "REPLICATION"], route) + all_results = await glide_client.custom_command(["INFO", "REPLICATION"], route) assert isinstance(all_results, dict) assert len(all_results) == expected_num_of_results primary_count = 0 replica_count = 0 for _, info_res in all_results.items(): + assert isinstance(info_res, bytes) + info_res = info_res.decode() assert "role:master" in info_res or "role:slave" in info_res if "role:master" in info_res: primary_count += 1 @@ -2041,38 +9652,43 @@ async def cluster_route_custom_command_multi_nodes( @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_route_custom_command_all_nodes( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): - await self.cluster_route_custom_command_multi_nodes(redis_client, AllNodes()) + await self.cluster_route_custom_command_multi_nodes(glide_client, AllNodes()) @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_route_custom_command_all_primaries( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): await self.cluster_route_custom_command_multi_nodes( - redis_client, AllPrimaries() + glide_client, AllPrimaries() ) @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_route_custom_command_random_node( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): - info_res = await redis_client.custom_command( + info_res = await glide_client.custom_command( ["INFO", "REPLICATION"], RandomNode() ) + assert isinstance(info_res, bytes) + info_res = info_res.decode() assert type(info_res) is str assert "role:master" in info_res or "role:slave" in info_res async def cluster_route_custom_command_slot_route( - self, redis_client: RedisClusterClient, is_slot_key: bool + self, glide_client: GlideClusterClient, is_slot_key: bool ): route_class = SlotKeyRoute if is_slot_key else SlotIdRoute route_second_arg = "foo" if is_slot_key else 4000 - primary_res = await redis_client.custom_command( + primary_res = await glide_client.custom_command( ["CLUSTER", "NODES"], route_class(SlotType.PRIMARY, route_second_arg) ) + assert isinstance(primary_res, bytes) + primary_res = primary_res.decode() + assert type(primary_res) is str assert "myself,master" in primary_res expected_primary_node_id = "" @@ -2080,9 +9696,12 @@ async def cluster_route_custom_command_slot_route( if "myself" in node_line: expected_primary_node_id = node_line.split(" ")[0] - replica_res = await redis_client.custom_command( + replica_res = await glide_client.custom_command( ["CLUSTER", "NODES"], route_class(SlotType.REPLICA, route_second_arg) ) + assert isinstance(replica_res, bytes) + replica_res = replica_res.decode() + assert isinstance(replica_res, str) assert "myself,slave" in replica_res for node_line in replica_res: @@ -2093,28 +9712,27 @@ async def cluster_route_custom_command_slot_route( @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_route_custom_command_slot_key_route( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): - await self.cluster_route_custom_command_slot_route(redis_client, True) + await self.cluster_route_custom_command_slot_route(glide_client, True) @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_route_custom_command_slot_id_route( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): - await self.cluster_route_custom_command_slot_route(redis_client, False) + await self.cluster_route_custom_command_slot_route(glide_client, False) @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_info_random_route(self, redis_client: RedisClusterClient): - info = await redis_client.info([InfoSection.SERVER], RandomNode()) - assert isinstance(info, str) - assert "# Server" in info + async def test_info_random_route(self, glide_client: GlideClusterClient): + info = await glide_client.info([InfoSection.SERVER], RandomNode()) + assert b"# Server" in info @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_route_by_address_reaches_correct_node( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): # returns the line that contains the word "myself", up to that point. This is done because the values after it might change with time. def clean_result(value: TResult): @@ -2126,48 +9744,390 @@ def clean_result(value: TResult): f"Couldn't find 'myself' in the cluster nodes output: {value}" ) - cluster_nodes = clean_result( - await redis_client.custom_command(["cluster", "nodes"], RandomNode()) + cluster_nodes = await glide_client.custom_command( + ["cluster", "nodes"], RandomNode() ) + assert isinstance(cluster_nodes, bytes) + cluster_nodes = clean_result(cluster_nodes.decode()) + assert isinstance(cluster_nodes, str) host = cluster_nodes.split(" ")[1].split("@")[0] - second_result = clean_result( - await redis_client.custom_command( - ["cluster", "nodes"], ByAddressRoute(host) - ) + second_result = await glide_client.custom_command( + ["cluster", "nodes"], ByAddressRoute(host) ) + assert isinstance(second_result, bytes) + second_result = clean_result(second_result.decode()) assert cluster_nodes == second_result host, port = host.split(":") port_as_int = int(port) - third_result = clean_result( - await redis_client.custom_command( - ["cluster", "nodes"], ByAddressRoute(host, port_as_int) - ) + third_result = await glide_client.custom_command( + ["cluster", "nodes"], ByAddressRoute(host, port_as_int) ) + assert isinstance(third_result, bytes) + third_result = clean_result(third_result.decode()) assert cluster_nodes == third_result @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_cluster_fail_routing_by_address_if_no_port_is_provided( - self, redis_client: RedisClusterClient + self, glide_client: GlideClusterClient ): - with pytest.raises(RequestError) as e: - await redis_client.info(route=ByAddressRoute("foo")) + with pytest.raises(RequestError): + await glide_client.info(route=ByAddressRoute("foo")) + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_flushdb(self, glide_client: GlideClusterClient): + min_version = "6.2.0" + key = f"{{key}}-1{get_random_string(5)}" + value = get_random_string(5) + + await glide_client.set(key, value) + assert await glide_client.dbsize() > 0 + assert await glide_client.flushdb(route=AllPrimaries()) == OK + assert await glide_client.dbsize() == 0 + + await glide_client.set(key, value) + assert await glide_client.dbsize() > 0 + assert await glide_client.flushdb(FlushMode.ASYNC, AllPrimaries()) == OK + assert await glide_client.dbsize() == 0 + + if not await check_if_server_version_lt(glide_client, min_version): + await glide_client.set(key, value) + assert await glide_client.dbsize() > 0 + assert await glide_client.flushdb(FlushMode.SYNC, AllPrimaries()) == OK + assert await glide_client.dbsize() == 0 -@pytest.mark.asyncio -class TestExceptions: @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_timeout_exception_with_blpop(self, redis_client: TRedisClient): - key = get_random_string(10) - with pytest.raises(TimeoutError): - await redis_client.custom_command(["BLPOP", key, "1"]) + async def test_sscan(self, glide_client: GlideClusterClient): + key1 = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + initial_cursor = "0" + result_cursor_index = 0 + result_collection_index = 1 + default_count = 10 + num_members: List[TEncodable] = list( + map(str, range(50000)) + ) # Use large dataset to force an iterative cursor. + char_members: List[TEncodable] = ["a", "b", "c", "d", "e"] + + # Empty set + result = await glide_client.sscan(key1, initial_cursor) + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + + # Negative cursor + result = await glide_client.sscan(key1, "-1") + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + + # Result contains the whole set + assert await glide_client.sadd(key1, char_members) == len(char_members) + result = await glide_client.sscan(key1, initial_cursor) + assert result[result_cursor_index] == initial_cursor.encode() + assert len(result[result_collection_index]) == len(char_members) + assert set(result[result_collection_index]).issubset( + cast(list, convert_string_to_bytes_object(char_members)) + ) + + result = await glide_client.sscan(key1, initial_cursor, match="a") + assert result[result_cursor_index] == initial_cursor.encode() + assert set(result[result_collection_index]).issubset(set([b"a"])) + + # Result contains a subset of the key + assert await glide_client.sadd(key1, num_members) == len(num_members) + result_cursor = "0" + result_values = set() # type: set[bytes] + result = cast( + list, + convert_bytes_to_string_object( + await glide_client.sscan(key1, result_cursor) + ), + ) + result_cursor = str(result[result_cursor_index]) + result_values.update(result[result_collection_index]) # type: ignore + + # 0 is returned for the cursor of the last iteration. + while result_cursor != "0": + next_result = cast( + list, + convert_bytes_to_string_object( + await glide_client.sscan(key1, result_cursor) + ), + ) + next_result_cursor = str(next_result[result_cursor_index]) + assert next_result_cursor != result_cursor + + assert not set(result[result_collection_index]).issubset( + set(next_result[result_collection_index]) + ) + result_values.update(next_result[result_collection_index]) + result = next_result + result_cursor = next_result_cursor + assert set(num_members).issubset(result_values) + assert set(char_members).issubset(result_values) + + # Test match pattern + result = await glide_client.sscan(key1, initial_cursor, match="*") + assert result[result_cursor_index] != "0" + assert len(result[result_collection_index]) >= default_count + + # Test count + result = await glide_client.sscan(key1, initial_cursor, count=20) + assert result[result_cursor_index] != "0" + assert len(result[result_collection_index]) >= 20 + + # Test count with match returns a non-empty list + result = await glide_client.sscan(key1, initial_cursor, match="1*", count=20) + assert result[result_cursor_index] != "0" + assert len(result[result_collection_index]) >= 0 + + # Exceptions + # Non-set key + assert await glide_client.set(key2, "test") == OK + with pytest.raises(RequestError): + await glide_client.sscan(key2, initial_cursor) + with pytest.raises(RequestError): + await glide_client.sscan(key2, initial_cursor, match="test", count=20) + + # Negative count + with pytest.raises(RequestError): + await glide_client.sscan(key2, initial_cursor, count=-1) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zscan(self, glide_client: GlideClusterClient): + key1 = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + initial_cursor = "0" + result_cursor_index = 0 + result_collection_index = 1 + default_count = 20 + num_map: Dict[TEncodable, float] = {} + num_map_with_str_scores = {} + for i in range(50000): # Use large dataset to force an iterative cursor. + num_map.update({"value " + str(i): i}) + num_map_with_str_scores.update({"value " + str(i): str(i)}) + char_map: Mapping[TEncodable, float] = {"a": 0, "b": 1, "c": 2, "d": 3, "e": 4} + char_map_with_str_scores = { + "a": "0", + "b": "1", + "c": "2", + "d": "3", + "e": "4", + } + + convert_list_to_dict = lambda list: { + list[i]: list[i + 1] for i in range(0, len(list), 2) + } + + # Empty set + result = await glide_client.zscan(key1, initial_cursor) + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + + # Negative cursor + result = await glide_client.zscan(key1, "-1") + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + + # Result contains the whole set + assert await glide_client.zadd(key1, char_map) == len(char_map) + result = await glide_client.zscan(key1, initial_cursor) + result_collection = result[result_collection_index] + assert result[result_cursor_index] == initial_cursor.encode() + assert len(result_collection) == len(char_map) * 2 + assert convert_list_to_dict(result_collection) == cast( + list, convert_string_to_bytes_object(char_map_with_str_scores) + ) + + result = await glide_client.zscan(key1, initial_cursor, match="a") + result_collection = result[result_collection_index] + assert result[result_cursor_index] == initial_cursor.encode() + assert convert_list_to_dict(result_collection) == {b"a": b"0"} + + # Result contains a subset of the key + assert await glide_client.zadd(key1, num_map) == len(num_map) + full_result_map = {} + result = result = cast( + list, + convert_bytes_to_string_object( + await glide_client.zscan(key1, initial_cursor) + ), + ) + result_cursor = str(result[result_cursor_index]) + result_iteration_collection: Dict[str, str] = convert_list_to_dict( + result[result_collection_index] + ) + full_result_map.update(result_iteration_collection) + + # 0 is returned for the cursor of the last iteration. + while result_cursor != "0": + next_result = cast( + list, + convert_bytes_to_string_object( + await glide_client.zscan(key1, result_cursor) + ), + ) + next_result_cursor = next_result[result_cursor_index] + assert next_result_cursor != result_cursor + + next_result_collection = convert_list_to_dict( + next_result[result_collection_index] + ) + assert result_iteration_collection != next_result_collection + + full_result_map.update(next_result_collection) + result_iteration_collection = next_result_collection + result_cursor = next_result_cursor + num_map_with_str_scores.update(char_map_with_str_scores) + assert num_map_with_str_scores == full_result_map + + # Test match pattern + result = await glide_client.zscan(key1, initial_cursor, match="*") + assert result[result_cursor_index] != b"0" + assert len(result[result_collection_index]) >= default_count + + # Test count + result = await glide_client.zscan(key1, initial_cursor, count=20) + assert result[result_cursor_index] != b"0" + assert len(result[result_collection_index]) >= 20 + + # Test count with match returns a non-empty list + result = await glide_client.zscan(key1, initial_cursor, match="1*", count=20) + assert result[result_cursor_index] != b"0" + assert len(result[result_collection_index]) >= 0 + + # Exceptions + # Non-set key + assert await glide_client.set(key2, "test") == OK + with pytest.raises(RequestError): + await glide_client.zscan(key2, initial_cursor) + with pytest.raises(RequestError): + await glide_client.zscan(key2, initial_cursor, match="test", count=20) + + # Negative count + with pytest.raises(RequestError): + await glide_client.zscan(key2, initial_cursor, count=-1) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_hscan(self, glide_client: GlideClusterClient): + key1 = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + initial_cursor = "0" + result_cursor_index = 0 + result_collection_index = 1 + default_count = 20 + num_map: dict[TEncodable, TEncodable] = {} + for i in range(50000): # Use large dataset to force an iterative cursor. + num_map.update({"field " + str(i): "value " + str(i)}) + char_map: Dict[TEncodable, TEncodable] = { + "field a": "value a", + "field b": "value b", + "field c": "value c", + "field d": "value d", + "field e": "value e", + } + + convert_list_to_dict = lambda list: { + list[i]: list[i + 1] for i in range(0, len(list), 2) + } + + # Empty set + result = await glide_client.hscan(key1, initial_cursor) + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + + # Negative cursor + result = await glide_client.hscan(key1, "-1") + assert result[result_cursor_index] == initial_cursor.encode() + assert result[result_collection_index] == [] + + # Result contains the whole set + assert await glide_client.hset(key1, char_map) == len(char_map) + result = await glide_client.hscan(key1, initial_cursor) + result_collection = result[result_collection_index] + assert result[result_cursor_index] == initial_cursor.encode() + assert len(result_collection) == len(char_map) * 2 + assert convert_list_to_dict(result_collection) == cast( + dict, convert_string_to_bytes_object(char_map) # type: ignore + ) + + result = await glide_client.hscan(key1, initial_cursor, match="field a") + result_collection = result[result_collection_index] + assert result[result_cursor_index] == initial_cursor.encode() + assert convert_list_to_dict(result_collection) == {b"field a": b"value a"} + + # Result contains a subset of the key + assert await glide_client.hset(key1, num_map) == len(num_map) + full_result_map = {} + result = result = cast( + list, + convert_bytes_to_string_object( + await glide_client.hscan(key1, initial_cursor) + ), + ) + result_cursor = str(result[result_cursor_index]) + result_iteration_collection: Dict[str, str] = convert_list_to_dict( + result[result_collection_index] + ) + full_result_map.update(result_iteration_collection) + + # 0 is returned for the cursor of the last iteration. + while result_cursor != "0": + next_result = cast( + list, + convert_bytes_to_string_object( + await glide_client.hscan(key1, result_cursor) + ), + ) + next_result_cursor = next_result[result_cursor_index] + assert next_result_cursor != result_cursor + + next_result_collection = convert_list_to_dict( + next_result[result_collection_index] + ) + assert result_iteration_collection != next_result_collection + + full_result_map.update(next_result_collection) + result_iteration_collection = next_result_collection + result_cursor = next_result_cursor + num_map.update(char_map) + assert num_map == full_result_map + + # Test match pattern + result = await glide_client.hscan(key1, initial_cursor, match="*") + assert result[result_cursor_index] != b"0" + assert len(result[result_collection_index]) >= default_count + + # Test count + result = await glide_client.hscan(key1, initial_cursor, count=20) + assert result[result_cursor_index] != b"0" + assert len(result[result_collection_index]) >= 20 + + # Test count with match returns a non-empty list + result = await glide_client.hscan(key1, initial_cursor, match="1*", count=20) + assert result[result_cursor_index] != b"0" + assert len(result[result_collection_index]) >= 0 + + # Exceptions + # Non-hash key + assert await glide_client.set(key2, "test") == OK + with pytest.raises(RequestError): + await glide_client.hscan(key2, initial_cursor) + with pytest.raises(RequestError): + await glide_client.hscan(key2, initial_cursor, match="test", count=20) + + # Negative count + with pytest.raises(RequestError): + await glide_client.hscan(key2, initial_cursor, count=-1) @pytest.mark.asyncio @@ -2175,22 +10135,102 @@ class TestScripts: @pytest.mark.smoke_test @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_script(self, redis_client: TRedisClient): + async def test_script(self, glide_client: TGlideClient): key1 = get_random_string(10) key2 = get_random_string(10) script = Script("return 'Hello'") - assert await redis_client.invoke_script(script) == "Hello" + assert await glide_client.invoke_script(script) == "Hello".encode() script = Script("return redis.call('SET', KEYS[1], ARGV[1])") assert ( - await redis_client.invoke_script(script, keys=[key1], args=["value1"]) + await glide_client.invoke_script(script, keys=[key1], args=["value1"]) == "OK" ) # Reuse the same script with different parameters. assert ( - await redis_client.invoke_script(script, keys=[key2], args=["value2"]) + await glide_client.invoke_script(script, keys=[key2], args=["value2"]) == "OK" ) script = Script("return redis.call('GET', KEYS[1])") - assert await redis_client.invoke_script(script, keys=[key1]) == "value1" - assert await redis_client.invoke_script(script, keys=[key2]) == "value2" + assert ( + await glide_client.invoke_script(script, keys=[key1]) == "value1".encode() + ) + assert ( + await glide_client.invoke_script(script, keys=[key2]) == "value2".encode() + ) + + @pytest.mark.smoke_test + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_script_binary(self, glide_client: TGlideClient): + key1 = bytes(get_random_string(10), "utf-8") + key2 = bytes(get_random_string(10), "utf-8") + script = Script(bytes("return 'Hello'", "utf-8")) + assert await glide_client.invoke_script(script) == "Hello".encode() + + script = Script(bytes("return redis.call('SET', KEYS[1], ARGV[1])", "utf-8")) + assert ( + await glide_client.invoke_script( + script, keys=[key1], args=[bytes("value1", "utf-8")] + ) + == "OK" + ) + # Reuse the same script with different parameters. + assert ( + await glide_client.invoke_script( + script, keys=[key2], args=[bytes("value2", "utf-8")] + ) + == "OK" + ) + script = Script(bytes("return redis.call('GET', KEYS[1])", "utf-8")) + assert ( + await glide_client.invoke_script(script, keys=[key1]) == "value1".encode() + ) + assert ( + await glide_client.invoke_script(script, keys=[key2]) == "value2".encode() + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_script_large_keys_no_args(self, request, cluster_mode, protocol): + glide_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=5000 + ) + length = 2**13 # 8kb + key = "0" * length + script = Script("return KEYS[1]") + assert await glide_client.invoke_script(script, keys=[key]) == key.encode() + await glide_client.close() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_script_large_args_no_keys(self, request, cluster_mode, protocol): + glide_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=5000 + ) + length = 2**12 # 4kb + arg1 = "0" * length + arg2 = "1" * length + + script = Script("return ARGV[2]") + assert ( + await glide_client.invoke_script(script, args=[arg1, arg2]) == arg2.encode() + ) + await glide_client.close() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_script_large_keys_and_args(self, request, cluster_mode, protocol): + glide_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=5000 + ) + length = 2**12 # 4kb + key = "0" * length + arg = "1" * length + + script = Script("return KEYS[1]") + assert ( + await glide_client.invoke_script(script, keys=[key], args=[arg]) + == key.encode() + ) + await glide_client.close() diff --git a/python/python/tests/test_config.py b/python/python/tests/test_config.py index ccd4d82a77..93c280245f 100644 --- a/python/python/tests/test_config.py +++ b/python/python/tests/test_config.py @@ -1,8 +1,8 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 from glide.config import ( BaseClientConfiguration, - ClusterClientConfiguration, + GlideClusterClientConfiguration, NodeAddress, PeriodicChecksManualInterval, PeriodicChecksStatus, @@ -38,7 +38,7 @@ def test_convert_to_protobuf(): def test_periodic_checks_interval_to_protobuf(): - config = ClusterClientConfiguration( + config = GlideClusterClientConfiguration( [NodeAddress("127.0.0.1")], ) request = config._create_a_protobuf_conn_request(cluster_mode=True) diff --git a/python/python/tests/test_proto_coded.py b/python/python/tests/test_proto_coded.py index db8bd1118f..3834e5d1dc 100644 --- a/python/python/tests/test_proto_coded.py +++ b/python/python/tests/test_proto_coded.py @@ -1,21 +1,23 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import pytest -from glide.protobuf.redis_request_pb2 import RedisRequest, RequestType +from glide.protobuf.command_request_pb2 import CommandRequest, RequestType from glide.protobuf.response_pb2 import Response from glide.protobuf_codec import PartialMessageException, ProtobufCodec class TestProtobufCodec: def test_encode_decode_delimited(self): - request = RedisRequest() + request = CommandRequest() request.callback_idx = 1 - request.single_command.request_type = RequestType.SetString + request.single_command.request_type = RequestType.Set args = [ "foo", "bar", ] - request.single_command.args_array.args[:] = args + + bytes_args = [bytes(elem, encoding="utf8") for elem in args] + request.single_command.args_array.args[:] = bytes_args b_arr = bytearray() ProtobufCodec.encode_delimited(b_arr, request) msg_len_varint = int(b_arr[0]) @@ -24,12 +26,12 @@ def test_encode_decode_delimited(self): offset = 0 b_arr_view = memoryview(b_arr) parsed_request, new_offset = ProtobufCodec.decode_delimited( - b_arr, b_arr_view, offset, RedisRequest + b_arr, b_arr_view, offset, CommandRequest ) assert new_offset == len(b_arr) assert parsed_request.callback_idx == 1 - assert parsed_request.single_command.request_type == RequestType.SetString - assert parsed_request.single_command.args_array.args == args + assert parsed_request.single_command.request_type == RequestType.Set + assert parsed_request.single_command.args_array.args == bytes_args def test_decode_partial_message_fails(self): response = Response() diff --git a/python/python/tests/test_pubsub.py b/python/python/tests/test_pubsub.py new file mode 100644 index 0000000000..23b5bb6709 --- /dev/null +++ b/python/python/tests/test_pubsub.py @@ -0,0 +1,2259 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +from __future__ import annotations + +import asyncio +from enum import IntEnum +from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast + +import pytest +from glide.async_commands.core import CoreCommands +from glide.config import ( + GlideClientConfiguration, + GlideClusterClientConfiguration, + ProtocolVersion, +) +from glide.constants import OK +from glide.exceptions import ConfigurationError +from glide.glide_client import BaseClient, GlideClient, GlideClusterClient, TGlideClient +from tests.conftest import create_client +from tests.utils.utils import check_if_server_version_lt, get_random_string + + +class MethodTesting(IntEnum): + """ + Enumeration for specifying the method of PUBSUB subscription. + """ + + Async = 0 + "Uses asynchronous subscription method." + Sync = 1 + "Uses synchronous subscription method." + Callback = 2 + "Uses callback-based subscription method." + + +async def create_two_clients_with_pubsub( + request, + cluster_mode, + client1_pubsub: Optional[Any] = None, + client2_pubsub: Optional[Any] = None, + protocol: ProtocolVersion = ProtocolVersion.RESP3, + timeout: Optional[int] = None, +) -> Tuple[TGlideClient, TGlideClient]: + """ + Sets 2 up clients for testing purposes with optional pubsub configuration. + + Args: + request: pytest request for creating a client. + cluster_mode: the cluster mode. + client1_pubsub: pubsub configuration subscription for the first client. + client2_pubsub: pubsub configuration subscription for the second client. + protocol: what protocol to use, used for the test: `test_pubsub_resp2_raise_an_error`. + """ + cluster_mode_pubsub1, standalone_mode_pubsub1 = None, None + cluster_mode_pubsub2, standalone_mode_pubsub2 = None, None + if cluster_mode: + cluster_mode_pubsub1 = client1_pubsub + cluster_mode_pubsub2 = client2_pubsub + else: + standalone_mode_pubsub1 = client1_pubsub + standalone_mode_pubsub2 = client2_pubsub + + client1 = await create_client( + request, + cluster_mode=cluster_mode, + cluster_mode_pubsub=cluster_mode_pubsub1, + standalone_mode_pubsub=standalone_mode_pubsub1, + protocol=protocol, + timeout=timeout, + ) + try: + client2 = await create_client( + request, + cluster_mode=cluster_mode, + cluster_mode_pubsub=cluster_mode_pubsub2, + standalone_mode_pubsub=standalone_mode_pubsub2, + protocol=protocol, + timeout=timeout, + ) + except Exception as e: + await client1.close() + raise e + + return client1, client2 + + +def decode_pubsub_msg(msg: Optional[CoreCommands.PubSubMsg]) -> CoreCommands.PubSubMsg: + if not msg: + return CoreCommands.PubSubMsg("", "", None) + string_msg = cast(bytes, msg.message).decode() + string_channel = cast(bytes, msg.channel).decode() + string_pattern = cast(bytes, msg.pattern).decode() if msg.pattern else None + decoded_msg = CoreCommands.PubSubMsg(string_msg, string_channel, string_pattern) + return decoded_msg + + +async def get_message_by_method( + method: MethodTesting, + client: TGlideClient, + messages: Optional[List[CoreCommands.PubSubMsg]] = None, + index: Optional[int] = None, +): + if method == MethodTesting.Async: + return decode_pubsub_msg(await client.get_pubsub_message()) + elif method == MethodTesting.Sync: + return decode_pubsub_msg(client.try_get_pubsub_message()) + assert messages and (index is not None) + return decode_pubsub_msg(messages[index]) + + +async def check_no_messages_left( + method, + client: TGlideClient, + callback: Optional[List[Any]] = None, + expected_callback_messages_count: int = 0, +): + if method == MethodTesting.Async: + # assert there are no messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(client.get_pubsub_message(), timeout=3) + elif method == MethodTesting.Sync: + assert client.try_get_pubsub_message() is None + else: + assert callback is not None + assert len(callback) == expected_callback_messages_count + + +def create_pubsub_subscription( + cluster_mode, + cluster_channels_and_patterns: Dict[ + GlideClusterClientConfiguration.PubSubChannelModes, Set[str] + ], + standalone_channels_and_patterns: Dict[ + GlideClientConfiguration.PubSubChannelModes, Set[str] + ], + callback=None, + context=None, +): + if cluster_mode: + return GlideClusterClientConfiguration.PubSubSubscriptions( + channels_and_patterns=cluster_channels_and_patterns, + callback=callback, + context=context, + ) + return GlideClientConfiguration.PubSubSubscriptions( + channels_and_patterns=standalone_channels_and_patterns, + callback=callback, + context=context, + ) + + +def new_message(msg: CoreCommands.PubSubMsg, context: Any): + received_messages: List[CoreCommands.PubSubMsg] = context + received_messages.append(msg) + + +async def client_cleanup( + client: Optional[Union[GlideClient, GlideClusterClient]], + cluster_mode_subs: Optional[ + GlideClusterClientConfiguration.PubSubSubscriptions + ] = None, +): + """ + This function tries its best to clear state assosiated with client + Its explicitly calls client.close() and deletes the object + In addition, it tries to clean up cluster mode subsciptions since it was found the closing the client via close() is not enough. + Note that unsubscribing is not feasible in the current implementation since its unknown on which node the subs are configured + """ + + if client is None: + return + + if cluster_mode_subs: + for ( + channel_type, + channel_patterns, + ) in cluster_mode_subs.channels_and_patterns.items(): + if channel_type == GlideClusterClientConfiguration.PubSubChannelModes.Exact: + cmd = "UNSUBSCRIBE" + elif ( + channel_type + == GlideClusterClientConfiguration.PubSubChannelModes.Pattern + ): + cmd = "PUNSUBSCRIBE" + elif not await check_if_server_version_lt(client, "7.0.0"): + cmd = "SUNSUBSCRIBE" + else: + # disregard sharded config for versions < 7.0.0 + continue + + for channel_patern in channel_patterns: + await client.custom_command([cmd, channel_patern]) + + await client.close() + del client + # The closure is not completed in the glide-core instantly + await asyncio.sleep(1) + + +@pytest.mark.asyncio +class TestPubSub: + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_exact_happy_path( + self, + request, + cluster_mode: bool, + method: MethodTesting, + ): + """ + Tests the basic happy path for exact PUBSUB functionality. + + This test covers the basic PUBSUB flow using three different methods: + Async, Sync, and Callback. It verifies that a message published to a + specific channel is correctly received by a subscriber. + """ + listening_client, publishing_client = None, None + try: + channel = get_random_string(10) + message = get_random_string(5) + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel}}, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + # allow the message to propagate + await asyncio.sleep(1) + + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, 0 + ) + + assert pubsub_msg.message == message + assert pubsub_msg.channel == channel + assert pubsub_msg.pattern is None + + await check_no_messages_left(method, listening_client, callback_messages, 1) + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_exact_happy_path_coexistence( + self, request, cluster_mode: bool + ): + """ + Tests the coexistence of async and sync message retrieval methods in exact PUBSUB. + + This test covers the scenario where messages are published to a channel + and received using both async and sync methods to ensure that both methods + can coexist and function correctly. + """ + listening_client, publishing_client = None, None + try: + channel = get_random_string(10) + message = get_random_string(5) + message2 = get_random_string(7) + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel}}, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + for msg in [message, message2]: + result = await publishing_client.publish(msg, channel) + if cluster_mode: + assert result == 1 + + # allow the message to propagate + await asyncio.sleep(1) + + async_msg_res = await listening_client.get_pubsub_message() + sync_msg_res = listening_client.try_get_pubsub_message() + assert sync_msg_res + async_msg = decode_pubsub_msg(async_msg_res) + sync_msg = decode_pubsub_msg(sync_msg_res) + + assert async_msg.message in [message, message2] + assert async_msg.channel == channel + assert async_msg.pattern is None + + assert sync_msg.message in [message, message2] + assert sync_msg.channel == channel + assert sync_msg.pattern is None + # we do not check the order of the messages, but we can check that we received both messages once + assert not sync_msg.message == async_msg.message + + # assert there are no messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(listening_client.get_pubsub_message(), timeout=3) + + assert listening_client.try_get_pubsub_message() is None + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_exact_happy_path_many_channels( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests publishing and receiving messages across many channels in exact PUBSUB. + + This test covers the scenario where multiple channels each receive their own + unique message. It verifies that messages are correctly published and received + using different retrieval methods: async, sync, and callback. + """ + listening_client, publishing_client = None, None + try: + NUM_CHANNELS = 256 + shard_prefix = "{same-shard}" + + # Create a map of channels to random messages with shard prefix + channels_and_messages = { + f"{shard_prefix}{get_random_string(10)}": get_random_string(5) + for _ in range(NUM_CHANNELS) + } + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: set( + channels_and_messages.keys() + ) + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: set( + channels_and_messages.keys() + ) + }, + callback=callback, + context=context, + ) + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + # Publish messages to each channel + for channel, message in channels_and_messages.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # Allow the messages to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly + for index in range(len(channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, index + ) + assert pubsub_msg.channel in channels_and_messages.keys() + assert pubsub_msg.message == channels_and_messages[pubsub_msg.channel] + assert pubsub_msg.pattern is None + del channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert channels_and_messages == {} + # check no messages left + await check_no_messages_left( + method, listening_client, callback_messages, NUM_CHANNELS + ) + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_exact_happy_path_many_channels_co_existence( + self, request, cluster_mode: bool + ): + """ + Tests publishing and receiving messages across many channels in exact PUBSUB, ensuring coexistence of async and sync retrieval methods. + + This test covers scenarios where multiple channels each receive their own unique message. + It verifies that messages are correctly published and received using both async and sync methods to ensure that both methods + can coexist and function correctly. + """ + listening_client, publishing_client = None, None + try: + NUM_CHANNELS = 256 + shard_prefix = "{same-shard}" + + # Create a map of channels to random messages with shard prefix + channels_and_messages = { + f"{shard_prefix}{get_random_string(10)}": get_random_string(5) + for _ in range(NUM_CHANNELS) + } + + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: set( + channels_and_messages.keys() + ) + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: set( + channels_and_messages.keys() + ) + }, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + # Publish messages to each channel + for channel, message in channels_and_messages.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # Allow the messages to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly by each method + for index in range(len(channels_and_messages)): + method = MethodTesting.Async if index % 2 else MethodTesting.Sync + pubsub_msg = await get_message_by_method(method, listening_client) + + assert pubsub_msg.channel in channels_and_messages.keys() + assert pubsub_msg.message == channels_and_messages[pubsub_msg.channel] + assert pubsub_msg.pattern is None + del channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert channels_and_messages == {} + # assert there are no messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(listening_client.get_pubsub_message(), timeout=3) + + assert listening_client.try_get_pubsub_message() is None + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_sharded_pubsub( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Test sharded PUBSUB functionality with different message retrieval methods. + + This test covers the sharded PUBSUB flow using three different methods: + Async, Sync, and Callback. It verifies that a message published to a + specific sharded channel is correctly received by a subscriber. + """ + listening_client, publishing_client = None, None + try: + channel = get_random_string(10) + message = get_random_string(5) + publish_response = 1 + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Sharded: {channel}}, + {}, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + min_version = "7.0.0" + if await check_if_server_version_lt(publishing_client, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") + + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == publish_response + ) + + # allow the message to propagate + await asyncio.sleep(1) + + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, 0 + ) + assert pubsub_msg.message == message + assert pubsub_msg.channel == channel + assert pubsub_msg.pattern is None + + # assert there are no messages to read + await check_no_messages_left(method, listening_client, callback_messages, 1) + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_sharded_pubsub_co_existence(self, request, cluster_mode: bool): + """ + Test sharded PUBSUB with co-existence of multiple messages. + + This test verifies the behavior of sharded PUBSUB when multiple messages are published + to the same sharded channel. It ensures that both async and sync methods of message retrieval + function correctly in this scenario. + + It covers the scenario where messages are published to a sharded channel and received using + both async and sync methods. This ensures that the asynchronous and synchronous message + retrieval methods can coexist without interfering with each other and operate as expected. + """ + listening_client, publishing_client = None, None + try: + channel = get_random_string(10) + message = get_random_string(5) + message2 = get_random_string(7) + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Sharded: {channel}}, + {}, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + min_version = "7.0.0" + if await check_if_server_version_lt(publishing_client, min_version): + pytest.skip(reason=f"Valkey version required >= {min_version}") + + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == 1 + ) + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message2, channel, sharded=True + ) + == 1 + ) + + # allow the messages to propagate + await asyncio.sleep(1) + + async_msg_res = await listening_client.get_pubsub_message() + sync_msg_res = listening_client.try_get_pubsub_message() + assert sync_msg_res + async_msg = decode_pubsub_msg(async_msg_res) + sync_msg = decode_pubsub_msg(sync_msg_res) + + assert async_msg.message in [message, message2] + assert async_msg.channel == channel + assert async_msg.pattern is None + + assert sync_msg.message in [message, message2] + assert sync_msg.channel == channel + assert sync_msg.pattern is None + # we do not check the order of the messages, but we can check that we received both messages once + assert not sync_msg.message == async_msg.message + + # assert there are no messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(listening_client.get_pubsub_message(), timeout=3) + + assert listening_client.try_get_pubsub_message() is None + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_sharded_pubsub_many_channels( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Test sharded PUBSUB with multiple channels and different message retrieval methods. + + This test verifies the behavior of sharded PUBSUB when multiple messages are published + across multiple sharded channels. It covers three different message retrieval methods: + Async, Sync, and Callback. + """ + listening_client, publishing_client = None, None + try: + NUM_CHANNELS = 256 + shard_prefix = "{same-shard}" + publish_response = 1 + + # Create a map of channels to random messages with shard prefix + channels_and_messages = { + f"{shard_prefix}{get_random_string(10)}": get_random_string(5) + for _ in range(NUM_CHANNELS) + } + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: set( + channels_and_messages.keys() + ) + }, + {}, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + min_version = "7.0.0" + if await check_if_server_version_lt(publishing_client, min_version): + pytest.skip(reason=f"Redis version required >= {min_version}") + + # Publish messages to each channel + for channel, message in channels_and_messages.items(): + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == publish_response + ) + + # Allow the messages to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly + for index in range(len(channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, index + ) + assert pubsub_msg.channel in channels_and_messages.keys() + assert pubsub_msg.message == channels_and_messages[pubsub_msg.channel] + assert pubsub_msg.pattern is None + del channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert channels_and_messages == {} + + # Assert there are no more messages to read + await check_no_messages_left( + method, listening_client, callback_messages, NUM_CHANNELS + ) + + finally: + if listening_client: + await client_cleanup( + listening_client, pub_sub if cluster_mode else None + ) + if publishing_client: + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_pattern( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Test PUBSUB with pattern subscription using different message retrieval methods. + + This test verifies the behavior of PUBSUB when subscribing to a pattern and receiving + messages using three different methods: Async, Sync, and Callback. + """ + listening_client, publishing_client = None, None + try: + PATTERN = "{{{}}}:{}".format("channel", "*") + channels = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 5 + ), + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 5 + ), + } + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + callback=callback, + context=context, + ) + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + for channel, message in channels.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # allow the message to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly + for index in range(len(channels)): + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, index + ) + assert pubsub_msg.channel in channels.keys() + assert pubsub_msg.message == channels[pubsub_msg.channel] + assert pubsub_msg.pattern == PATTERN + del channels[pubsub_msg.channel] + + # check that we received all messages + assert channels == {} + + await check_no_messages_left(method, listening_client, callback_messages, 2) + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_pattern_co_existence(self, request, cluster_mode: bool): + """ + Tests the coexistence of async and sync message retrieval methods in pattern-based PUBSUB. + + This test covers the scenario where messages are published to a channel that match a specified pattern + and received using both async and sync methods to ensure that both methods + can coexist and function correctly. + """ + listening_client, publishing_client = None, None + try: + PATTERN = "{{{}}}:{}".format("channel", "*") + channels = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 5 + ), + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 5 + ), + } + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + for channel, message in channels.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # allow the message to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly by each method + for index in range(len(channels)): + method = MethodTesting.Async if index % 2 else MethodTesting.Sync + pubsub_msg = await get_message_by_method(method, listening_client) + + assert pubsub_msg.channel in channels.keys() + assert pubsub_msg.message == channels[pubsub_msg.channel] + assert pubsub_msg.pattern == PATTERN + del channels[pubsub_msg.channel] + + # check that we received all messages + assert channels == {} + + # assert there are no more messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(listening_client.get_pubsub_message(), timeout=3) + + assert listening_client.try_get_pubsub_message() is None + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_pattern_many_channels( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests publishing and receiving messages across many channels in pattern-based PUBSUB. + + This test covers the scenario where messages are published to multiple channels that match a specified pattern + and received. It verifies that messages are correctly published and received + using different retrieval methods: async, sync, and callback. + """ + listening_client, publishing_client = None, None + try: + NUM_CHANNELS = 256 + PATTERN = "{{{}}}:{}".format("channel", "*") + channels = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 5 + ) + for _ in range(NUM_CHANNELS) + } + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + callback=callback, + context=context, + ) + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub + ) + + for channel, message in channels.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # allow the message to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly + for index in range(len(channels)): + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, index + ) + assert pubsub_msg.channel in channels.keys() + assert pubsub_msg.message == channels[pubsub_msg.channel] + assert pubsub_msg.pattern == PATTERN + del channels[pubsub_msg.channel] + + # check that we received all messages + assert channels == {} + + await check_no_messages_left( + method, listening_client, callback_messages, NUM_CHANNELS + ) + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_combined_exact_and_pattern_one_client( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests combined exact and pattern PUBSUB with one client. + + This test verifies that a single client can correctly handle both exact and pattern PUBSUB + subscriptions. It covers the following scenarios: + - Subscribing to multiple channels with exact names and verifying message reception. + - Subscribing to channels using a pattern and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + """ + listening_client, publishing_client = None, None + try: + NUM_CHANNELS = 256 + PATTERN = "{{{}}}:{}".format("pattern", "*") + + # Create dictionaries of channels and their corresponding messages + exact_channels_and_messages = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 10 + ) + for _ in range(NUM_CHANNELS) + } + pattern_channels_and_messages = { + "{{{}}}:{}".format("pattern", get_random_string(5)): get_random_string( + 5 + ) + for _ in range(NUM_CHANNELS) + } + + all_channels_and_messages = { + **exact_channels_and_messages, + **pattern_channels_and_messages, + } + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + # Setup PUBSUB for exact channels + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ), + GlideClusterClientConfiguration.PubSubChannelModes.Pattern: { + PATTERN + }, + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ), + GlideClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}, + }, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub_exact, + ) + + # Publish messages to all channels + for channel, message in all_channels_and_messages.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # allow the message to propagate + await asyncio.sleep(1) + + # Check if all messages are received correctly + for index in range(len(all_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, index + ) + pattern = ( + PATTERN + if pubsub_msg.channel in pattern_channels_and_messages.keys() + else None + ) + assert pubsub_msg.channel in all_channels_and_messages.keys() + assert ( + pubsub_msg.message == all_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern == pattern + del all_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert all_channels_and_messages == {} + + await check_no_messages_left( + method, listening_client, callback_messages, NUM_CHANNELS * 2 + ) + finally: + await client_cleanup( + listening_client, pub_sub_exact if cluster_mode else None + ) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_combined_exact_and_pattern_multiple_clients( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests combined exact and pattern PUBSUB with multiple clients, one for each subscription. + + This test verifies that separate clients can correctly handle both exact and pattern PUBSUB + subscriptions. It covers the following scenarios: + - Subscribing to multiple channels with exact names and verifying message reception. + - Subscribing to channels using a pattern and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + - Verifying that no messages are left unread. + - Properly unsubscribing from all channels to avoid interference with other tests. + """ + ( + listening_client_exact, + publishing_client, + listening_client_pattern, + client_dont_care, + ) = (None, None, None, None) + try: + NUM_CHANNELS = 256 + PATTERN = "{{{}}}:{}".format("pattern", "*") + + # Create dictionaries of channels and their corresponding messages + exact_channels_and_messages = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 10 + ) + for _ in range(NUM_CHANNELS) + } + pattern_channels_and_messages = { + "{{{}}}:{}".format("pattern", get_random_string(5)): get_random_string( + 5 + ) + for _ in range(NUM_CHANNELS) + } + + all_channels_and_messages = { + **exact_channels_and_messages, + **pattern_channels_and_messages, + } + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + # Setup PUBSUB for exact channels + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ) + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ) + }, + callback=callback, + context=context, + ) + + listening_client_exact, publishing_client = ( + await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub_exact, + ) + ) + + callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages_pattern + + # Setup PUBSUB for pattern channels + pub_sub_pattern = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + callback=callback, + context=context, + ) + + listening_client_pattern, client_dont_care = ( + await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_pattern + ) + ) + + # Publish messages to all channels + for channel, message in all_channels_and_messages.items(): + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + # allow the messages to propagate + await asyncio.sleep(1) + + # Verify messages for exact PUBSUB + for index in range(len(exact_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client_exact, callback_messages, index + ) + assert pubsub_msg.channel in exact_channels_and_messages.keys() + assert ( + pubsub_msg.message + == exact_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern is None + del exact_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert exact_channels_and_messages == {} + + # Verify messages for pattern PUBSUB + for index in range(len(pattern_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client_pattern, callback_messages_pattern, index + ) + assert pubsub_msg.channel in pattern_channels_and_messages.keys() + assert ( + pubsub_msg.message + == pattern_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern == PATTERN + del pattern_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert pattern_channels_and_messages == {} + + await check_no_messages_left( + method, listening_client_exact, callback_messages, NUM_CHANNELS + ) + await check_no_messages_left( + method, + listening_client_pattern, + callback_messages_pattern, + NUM_CHANNELS, + ) + + finally: + await client_cleanup( + listening_client_exact, pub_sub_exact if cluster_mode else None + ) + await client_cleanup(publishing_client, None) + await client_cleanup( + listening_client_pattern, pub_sub_pattern if cluster_mode else None + ) + await client_cleanup(client_dont_care, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_combined_exact_pattern_and_sharded_one_client( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests combined exact, pattern and sharded PUBSUB with one client. + + This test verifies that a single client can correctly handle both exact, pattern and sharded PUBSUB + subscriptions. It covers the following scenarios: + - Subscribing to multiple channels with exact names and verifying message reception. + - Subscribing to channels using a pattern and verifying message reception. + - Subscribing to channels using a with sharded subscription and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + """ + listening_client, publishing_client = None, None + try: + NUM_CHANNELS = 256 + PATTERN = "{{{}}}:{}".format("pattern", "*") + SHARD_PREFIX = "{same-shard}" + + # Create dictionaries of channels and their corresponding messages + exact_channels_and_messages = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 10 + ) + for _ in range(NUM_CHANNELS) + } + pattern_channels_and_messages = { + "{{{}}}:{}".format("pattern", get_random_string(5)): get_random_string( + 5 + ) + for _ in range(NUM_CHANNELS) + } + sharded_channels_and_messages = { + f"{SHARD_PREFIX}:{get_random_string(10)}": get_random_string(7) + for _ in range(NUM_CHANNELS) + } + + publish_response = 1 + + callback, context = None, None + callback_messages: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages + + # Setup PUBSUB for exact channels + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ), + GlideClusterClientConfiguration.PubSubChannelModes.Pattern: { + PATTERN + }, + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: set( + sharded_channels_and_messages.keys() + ), + }, + {}, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub_exact, + ) + + # Setup PUBSUB for sharded channels (Redis version > 7) + if await check_if_server_version_lt(publishing_client, "7.0.0"): + pytest.skip("Redis version required >= 7.0.0") + + # Publish messages to all channels + for channel, message in { + **exact_channels_and_messages, + **pattern_channels_and_messages, + }.items(): + assert ( + await publishing_client.publish(message, channel) + == publish_response + ) + + # Publish sharded messages to all channels + for channel, message in sharded_channels_and_messages.items(): + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == publish_response + ) + + # allow the messages to propagate + await asyncio.sleep(1) + + all_channels_and_messages = { + **exact_channels_and_messages, + **pattern_channels_and_messages, + **sharded_channels_and_messages, + } + # Check if all messages are received correctly + for index in range(len(all_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client, callback_messages, index + ) + pattern = ( + PATTERN + if pubsub_msg.channel in pattern_channels_and_messages.keys() + else None + ) + assert pubsub_msg.channel in all_channels_and_messages.keys() + assert ( + pubsub_msg.message == all_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern == pattern + del all_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert all_channels_and_messages == {} + + await check_no_messages_left( + method, listening_client, callback_messages, NUM_CHANNELS * 3 + ) + + finally: + await client_cleanup( + listening_client, pub_sub_exact if cluster_mode else None + ) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_combined_exact_pattern_and_sharded_multi_client( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests combined exact, pattern and sharded PUBSUB with multiple clients, one for each subscription. + + This test verifies that separate clients can correctly handle exact, pattern and sharded PUBSUB + subscriptions. It covers the following scenarios: + - Subscribing to multiple channels with exact names and verifying message reception. + - Subscribing to channels using a pattern and verifying message reception. + - Subscribing to channels using a sharded subscription and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + - Verifying that no messages are left unread. + - Properly unsubscribing from all channels to avoid interference with other tests. + """ + ( + listening_client_exact, + publishing_client, + listening_client_pattern, + listening_client_sharded, + ) = (None, None, None, None) + + ( + pub_sub_exact, + pub_sub_sharded, + pub_sub_pattern, + ) = (None, None, None) + + try: + NUM_CHANNELS = 256 + PATTERN = "{{{}}}:{}".format("pattern", "*") + SHARD_PREFIX = "{same-shard}" + + # Create dictionaries of channels and their corresponding messages + exact_channels_and_messages = { + "{{{}}}:{}".format("channel", get_random_string(5)): get_random_string( + 10 + ) + for _ in range(NUM_CHANNELS) + } + pattern_channels_and_messages = { + "{{{}}}:{}".format("pattern", get_random_string(5)): get_random_string( + 5 + ) + for _ in range(NUM_CHANNELS) + } + sharded_channels_and_messages = { + f"{SHARD_PREFIX}:{get_random_string(10)}": get_random_string(7) + for _ in range(NUM_CHANNELS) + } + + publish_response = 1 + + callback, context = None, None + callback_messages_exact: List[CoreCommands.PubSubMsg] = [] + callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + callback_messages_sharded: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages_exact + + # Setup PUBSUB for exact channels + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ) + }, + { + GlideClientConfiguration.PubSubChannelModes.Exact: set( + exact_channels_and_messages.keys() + ) + }, + callback=callback, + context=context, + ) + + listening_client_exact, publishing_client = ( + await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub_exact, + ) + ) + + # Setup PUBSUB for sharded channels (Valkey version > 7) + if await check_if_server_version_lt(publishing_client, "7.0.0"): + pytest.skip("Valkey version required >= 7.0.0") + + if method == MethodTesting.Callback: + context = callback_messages_pattern + + # Setup PUBSUB for pattern channels + pub_sub_pattern = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {PATTERN}}, + callback=callback, + context=context, + ) + + if method == MethodTesting.Callback: + context = callback_messages_sharded + + pub_sub_sharded = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: set( + sharded_channels_and_messages.keys() + ) + }, + {}, + callback=callback, + context=context, + ) + + listening_client_pattern, listening_client_sharded = ( + await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_pattern, pub_sub_sharded + ) + ) + + # Publish messages to all channels + for channel, message in { + **exact_channels_and_messages, + **pattern_channels_and_messages, + }.items(): + assert ( + await publishing_client.publish(message, channel) + == publish_response + ) + + # Publish sharded messages to all channels + for channel, message in sharded_channels_and_messages.items(): + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == publish_response + ) + + # allow the messages to propagate + await asyncio.sleep(1) + + # Verify messages for exact PUBSUB + for index in range(len(exact_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client_exact, callback_messages_exact, index + ) + assert pubsub_msg.channel in exact_channels_and_messages.keys() + assert ( + pubsub_msg.message + == exact_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern is None + del exact_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert exact_channels_and_messages == {} + + # Verify messages for pattern PUBSUB + for index in range(len(pattern_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client_pattern, callback_messages_pattern, index + ) + assert pubsub_msg.channel in pattern_channels_and_messages.keys() + assert ( + pubsub_msg.message + == pattern_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern == PATTERN + del pattern_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert pattern_channels_and_messages == {} + + # Verify messages for shaded PUBSUB + for index in range(len(sharded_channels_and_messages)): + pubsub_msg = await get_message_by_method( + method, listening_client_sharded, callback_messages_sharded, index + ) + assert pubsub_msg.channel in sharded_channels_and_messages.keys() + assert ( + pubsub_msg.message + == sharded_channels_and_messages[pubsub_msg.channel] + ) + assert pubsub_msg.pattern is None + del sharded_channels_and_messages[pubsub_msg.channel] + + # check that we received all messages + assert sharded_channels_and_messages == {} + + await check_no_messages_left( + method, listening_client_exact, callback_messages_exact, NUM_CHANNELS + ) + await check_no_messages_left( + method, + listening_client_pattern, + callback_messages_pattern, + NUM_CHANNELS, + ) + await check_no_messages_left( + method, + listening_client_sharded, + callback_messages_sharded, + NUM_CHANNELS, + ) + + finally: + await client_cleanup( + listening_client_exact, pub_sub_exact if cluster_mode else None + ) + await client_cleanup(publishing_client, None) + await client_cleanup( + listening_client_pattern, pub_sub_pattern if cluster_mode else None + ) + await client_cleanup( + listening_client_sharded, pub_sub_sharded if cluster_mode else None + ) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_combined_different_channels_with_same_name( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests combined PUBSUB with different channel modes using the same channel name. + One publishing clients, 3 listening clients, one for each mode. + + This test verifies that separate clients can correctly handle subscriptions for exact, pattern, and sharded channels with the same name. + It covers the following scenarios: + - Subscribing to an exact channel and verifying message reception. + - Subscribing to a pattern channel and verifying message reception. + - Subscribing to a sharded channel and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + - Verifying that no messages are left unread. + - Properly unsubscribing from all channels to avoid interference with other tests. + """ + ( + listening_client_exact, + publishing_client, + listening_client_pattern, + listening_client_sharded, + ) = (None, None, None, None) + + ( + pub_sub_exact, + pub_sub_sharded, + pub_sub_pattern, + ) = (None, None, None) + + try: + CHANNEL_NAME = "same-channel-name" + MESSAGE_EXACT = get_random_string(10) + MESSAGE_PATTERN = get_random_string(7) + MESSAGE_SHARDED = get_random_string(5) + + callback, context = None, None + callback_messages_exact: List[CoreCommands.PubSubMsg] = [] + callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + callback_messages_sharded: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context = callback_messages_exact + + # Setup PUBSUB for exact channel + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + CHANNEL_NAME + } + }, + {GlideClientConfiguration.PubSubChannelModes.Exact: {CHANNEL_NAME}}, + callback=callback, + context=context, + ) + + listening_client_exact, publishing_client = ( + await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub_exact, + ) + ) + + # (Valkey version > 7) + if await check_if_server_version_lt(publishing_client, "7.0.0"): + pytest.skip("Valkey version required >= 7.0.0") + + # Setup PUBSUB for pattern channel + if method == MethodTesting.Callback: + context = callback_messages_pattern + + # Setup PUBSUB for pattern channels + pub_sub_pattern = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Pattern: { + CHANNEL_NAME + } + }, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {CHANNEL_NAME}}, + callback=callback, + context=context, + ) + + if method == MethodTesting.Callback: + context = callback_messages_sharded + + pub_sub_sharded = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + CHANNEL_NAME + } + }, + {}, + callback=callback, + context=context, + ) + + listening_client_pattern, listening_client_sharded = ( + await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_pattern, pub_sub_sharded + ) + ) + + # Publish messages to each channel + assert await publishing_client.publish(MESSAGE_EXACT, CHANNEL_NAME) == 2 + assert await publishing_client.publish(MESSAGE_PATTERN, CHANNEL_NAME) == 2 + assert ( + await cast(GlideClusterClient, publishing_client).publish( + MESSAGE_SHARDED, CHANNEL_NAME, sharded=True + ) + == 1 + ) + + # allow the message to propagate + await asyncio.sleep(1) + + # Verify message for exact and pattern PUBSUB + for client, callback, pattern in [ # type: ignore + (listening_client_exact, callback_messages_exact, None), + (listening_client_pattern, callback_messages_pattern, CHANNEL_NAME), + ]: + pubsub_msg = await get_message_by_method(method, client, callback, 0) # type: ignore + + pubsub_msg2 = await get_message_by_method(method, client, callback, 1) # type: ignore + assert not pubsub_msg.message == pubsub_msg2.message + assert pubsub_msg2.message in [MESSAGE_PATTERN, MESSAGE_EXACT] + assert pubsub_msg.message in [MESSAGE_PATTERN, MESSAGE_EXACT] + assert pubsub_msg.channel == pubsub_msg2.channel == CHANNEL_NAME + assert pubsub_msg.pattern == pubsub_msg2.pattern == pattern + + # Verify message for sharded PUBSUB + pubsub_msg_sharded = await get_message_by_method( + method, listening_client_sharded, callback_messages_sharded, 0 + ) + assert pubsub_msg_sharded.message == MESSAGE_SHARDED + assert pubsub_msg_sharded.channel == CHANNEL_NAME + assert pubsub_msg_sharded.pattern is None + + await check_no_messages_left( + method, listening_client_exact, callback_messages_exact, 2 + ) + await check_no_messages_left( + method, listening_client_pattern, callback_messages_pattern, 2 + ) + await check_no_messages_left( + method, listening_client_sharded, callback_messages_sharded, 1 + ) + + finally: + await client_cleanup( + listening_client_exact, pub_sub_exact if cluster_mode else None + ) + await client_cleanup(publishing_client, None) + await client_cleanup( + listening_client_pattern, pub_sub_pattern if cluster_mode else None + ) + await client_cleanup( + listening_client_sharded, pub_sub_sharded if cluster_mode else None + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_two_publishing_clients_same_name( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests PUBSUB with two publishing clients using the same channel name. + One client uses pattern subscription, the other uses exact. + The clients publishes messages to each other, and to thyself. + + This test verifies that two separate clients can correctly publish to and handle subscriptions + for exact and pattern channels with the same name. It covers the following scenarios: + - Subscribing to an exact channel and verifying message reception. + - Subscribing to a pattern channel and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + - Verifying that no messages are left unread. + - Properly unsubscribing from all channels to avoid interference with other tests. + """ + client_exact, client_pattern = None, None + try: + CHANNEL_NAME = "channel-name" + MESSAGE_EXACT = get_random_string(10) + MESSAGE_PATTERN = get_random_string(7) + callback, context_exact, context_pattern = None, None, None + callback_messages_exact: List[CoreCommands.PubSubMsg] = [] + callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context_exact = callback_messages_exact + context_pattern = callback_messages_pattern + + # Setup PUBSUB for exact channel + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + CHANNEL_NAME + } + }, + {GlideClientConfiguration.PubSubChannelModes.Exact: {CHANNEL_NAME}}, + callback=callback, + context=context_exact, + ) + # Setup PUBSUB for pattern channels + pub_sub_pattern = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Pattern: { + CHANNEL_NAME + } + }, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {CHANNEL_NAME}}, + callback=callback, + context=context_pattern, + ) + + client_exact, client_pattern = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_exact, pub_sub_pattern + ) + + # Publish messages to each channel - both clients publishing + for msg in [MESSAGE_EXACT, MESSAGE_PATTERN]: + result = await client_pattern.publish(msg, CHANNEL_NAME) + if cluster_mode: + assert result == 2 + + # allow the message to propagate + await asyncio.sleep(1) + + # Verify message for exact and pattern PUBSUB + for client, callback, pattern in [ # type: ignore + (client_exact, callback_messages_exact, None), + (client_pattern, callback_messages_pattern, CHANNEL_NAME), + ]: + pubsub_msg = await get_message_by_method(method, client, callback, 0) # type: ignore + + pubsub_msg2 = await get_message_by_method(method, client, callback, 1) # type: ignore + assert not pubsub_msg.message == pubsub_msg2.message + assert pubsub_msg2.message in [MESSAGE_PATTERN, MESSAGE_EXACT] + assert pubsub_msg.message in [MESSAGE_PATTERN, MESSAGE_EXACT] + assert pubsub_msg.channel == pubsub_msg2.channel == CHANNEL_NAME + assert pubsub_msg.pattern == pubsub_msg2.pattern == pattern + + await check_no_messages_left( + method, client_pattern, callback_messages_pattern, 2 + ) + await check_no_messages_left( + method, client_exact, callback_messages_exact, 2 + ) + + finally: + await client_cleanup(client_exact, pub_sub_exact if cluster_mode else None) + await client_cleanup( + client_pattern, pub_sub_pattern if cluster_mode else None + ) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize( + "method", [MethodTesting.Async, MethodTesting.Sync, MethodTesting.Callback] + ) + async def test_pubsub_three_publishing_clients_same_name_with_sharded( + self, request, cluster_mode: bool, method: MethodTesting + ): + """ + Tests PUBSUB with 3 publishing clients using the same channel name. + One client uses pattern subscription, one uses exact, and one uses sharded. + + This test verifies that 3 separate clients can correctly publish to and handle subscriptions + for exact, sharded and pattern channels with the same name. It covers the following scenarios: + - Subscribing to an exact channel and verifying message reception. + - Subscribing to a pattern channel and verifying message reception. + - Subscribing to a sharded channel and verifying message reception. + - Ensuring that messages are correctly published and received using different retrieval methods (async, sync, callback). + - Verifying that no messages are left unread. + - Properly unsubscribing from all channels to avoid interference with other tests. + """ + client_exact, client_pattern, client_sharded, client_dont_care = ( + None, + None, + None, + None, + ) + try: + CHANNEL_NAME = "same-channel-name" + MESSAGE_EXACT = get_random_string(10) + MESSAGE_PATTERN = get_random_string(7) + MESSAGE_SHARDED = get_random_string(5) + publish_response = 2 if cluster_mode else OK + callback, context_exact, context_pattern, context_sharded = ( + None, + None, + None, + None, + ) + callback_messages_exact: List[CoreCommands.PubSubMsg] = [] + callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + callback_messages_sharded: List[CoreCommands.PubSubMsg] = [] + + if method == MethodTesting.Callback: + callback = new_message + context_exact = callback_messages_exact + context_pattern = callback_messages_pattern + context_sharded = callback_messages_sharded + + # Setup PUBSUB for exact channel + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Exact: { + CHANNEL_NAME + } + }, + {GlideClientConfiguration.PubSubChannelModes.Exact: {CHANNEL_NAME}}, + callback=callback, + context=context_exact, + ) + # Setup PUBSUB for pattern channels + pub_sub_pattern = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Pattern: { + CHANNEL_NAME + } + }, + {GlideClientConfiguration.PubSubChannelModes.Pattern: {CHANNEL_NAME}}, + callback=callback, + context=context_pattern, + ) + # Setup PUBSUB for pattern channels + pub_sub_sharded = create_pubsub_subscription( + cluster_mode, + { + GlideClusterClientConfiguration.PubSubChannelModes.Sharded: { + CHANNEL_NAME + } + }, + {}, + callback=callback, + context=context_sharded, + ) + + client_exact, client_pattern = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_exact, pub_sub_pattern + ) + client_sharded, client_dont_care = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_sharded + ) + # (Valkey version > 7) + if await check_if_server_version_lt(client_pattern, "7.0.0"): + pytest.skip("Valkey version required >= 7.0.0") + + # Publish messages to each channel - both clients publishing + assert ( + await client_pattern.publish(MESSAGE_EXACT, CHANNEL_NAME) + == publish_response + ) + assert ( + await client_sharded.publish(MESSAGE_PATTERN, CHANNEL_NAME) + == publish_response + ) + assert ( + await cast(GlideClusterClient, client_exact).publish( + MESSAGE_SHARDED, CHANNEL_NAME, sharded=True + ) + == 1 + ) + + # allow the message to propagate + await asyncio.sleep(1) + + # Verify message for exact and pattern PUBSUB + for client, callback, pattern in [ # type: ignore + (client_exact, callback_messages_exact, None), + (client_pattern, callback_messages_pattern, CHANNEL_NAME), + ]: + pubsub_msg = await get_message_by_method(method, client, callback, 0) # type: ignore + + pubsub_msg2 = await get_message_by_method(method, client, callback, 1) # type: ignore + assert not pubsub_msg.message == pubsub_msg2.message + assert pubsub_msg2.message in [MESSAGE_PATTERN, MESSAGE_EXACT] + assert pubsub_msg.message in [MESSAGE_PATTERN, MESSAGE_EXACT] + assert pubsub_msg.channel == pubsub_msg2.channel == CHANNEL_NAME + assert pubsub_msg.pattern == pubsub_msg2.pattern == pattern + + msg = await get_message_by_method( + method, client_sharded, callback_messages_sharded, 0 + ) + assert msg.message == MESSAGE_SHARDED + assert msg.channel == CHANNEL_NAME + assert msg.pattern is None + + await check_no_messages_left( + method, client_pattern, callback_messages_pattern, 2 + ) + await check_no_messages_left( + method, client_exact, callback_messages_exact, 2 + ) + await check_no_messages_left( + method, client_sharded, callback_messages_sharded, 1 + ) + + finally: + await client_cleanup(client_exact, pub_sub_exact if cluster_mode else None) + await client_cleanup( + client_pattern, pub_sub_pattern if cluster_mode else None + ) + await client_cleanup( + client_sharded, pub_sub_sharded if cluster_mode else None + ) + await client_cleanup(client_dont_care, None) + + @pytest.mark.skip( + reason="This test requires special configuration for client-output-buffer-limit for valkey-server and timeouts seems to vary across platforms and server versions" + ) + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_exact_max_size_message(self, request, cluster_mode: bool): + """ + Tests publishing and receiving maximum size messages in PUBSUB. + + This test verifies that very large messages (512MB - BulkString max size) can be published and received + correctly in both cluster and standalone modes. It ensures that the PUBSUB system + can handle maximum size messages without errors and that async and sync message + retrieval methods can coexist and function correctly. + + The test covers the following scenarios: + - Setting up PUBSUB subscription for a specific channel. + - Publishing two maximum size messages to the channel. + - Verifying that the messages are received correctly using both async and sync methods. + - Ensuring that no additional messages are left after the expected messages are received. + """ + channel = get_random_string(10) + message = "1" * 512 * 1024 * 1024 + message2 = "2" * 512 * 1024 * 1024 + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel}}, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub, + timeout=10000, + ) + + try: + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + + result = await publishing_client.publish(message2, channel) + if cluster_mode: + assert result == 1 + # allow the message to propagate + await asyncio.sleep(15) + + async_msg = await listening_client.get_pubsub_message() + assert async_msg.message == message.encode() + assert async_msg.channel == channel.encode() + assert async_msg.pattern is None + + sync_msg = listening_client.try_get_pubsub_message() + assert sync_msg + assert sync_msg.message == message2.encode() + assert sync_msg.channel == channel.encode() + assert sync_msg.pattern is None + + # assert there are no messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(listening_client.get_pubsub_message(), timeout=3) + + assert listening_client.try_get_pubsub_message() is None + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.skip( + reason="This test requires special configuration for client-output-buffer-limit for valkey-server and timeouts seems to vary across platforms and server versions" + ) + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_pubsub_sharded_max_size_message(self, request, cluster_mode: bool): + """ + Tests publishing and receiving maximum size messages in sharded PUBSUB. + + This test verifies that very large messages (512MB - BulkString max size) can be published and received + correctly. It ensures that the PUBSUB system + can handle maximum size messages without errors and that async and sync message + retrieval methods can coexist and function correctly. + + The test covers the following scenarios: + - Setting up PUBSUB subscription for a specific sharded channel. + - Publishing two maximum size messages to the channel. + - Verifying that the messages are received correctly using both async and sync methods. + - Ensuring that no additional messages are left after the expected messages are received. + """ + publishing_client, listening_client = None, None + try: + channel = get_random_string(10) + message = "1" * 512 * 1024 * 1024 + message2 = "2" * 512 * 1024 * 1024 + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Sharded: {channel}}, + {}, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, + cluster_mode, + pub_sub, + timeout=10000, + ) + + # (Redis version > 7) + if await check_if_server_version_lt(publishing_client, "7.0.0"): + pytest.skip("Redis version required >= 7.0.0") + + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == 1 + ) + + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message2, channel, sharded=True + ) + == 1 + ) + + # allow the message to propagate + await asyncio.sleep(15) + + async_msg = await listening_client.get_pubsub_message() + sync_msg = listening_client.try_get_pubsub_message() + assert sync_msg + + assert async_msg.message == message.encode() + assert async_msg.channel == channel.encode() + assert async_msg.pattern is None + + assert sync_msg.message == message2.encode() + assert sync_msg.channel == channel.encode() + assert sync_msg.pattern is None + + # assert there are no messages to read + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(listening_client.get_pubsub_message(), timeout=3) + + assert listening_client.try_get_pubsub_message() is None + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.skip( + reason="This test requires special configuration for client-output-buffer-limit for valkey-server and timeouts seems to vary across platforms and server versions" + ) + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_exact_max_size_message_callback( + self, request, cluster_mode: bool + ): + """ + Tests publishing and receiving maximum size messages in exact PUBSUB with callback method. + + This test verifies that very large messages (512MB - BulkString max size) can be published and received + correctly in both cluster and standalone modes. It ensures that the PUBSUB system + can handle maximum size messages without errors and that the callback message + retrieval method works as expected. + + The test covers the following scenarios: + - Setting up PUBSUB subscription for a specific channel with a callback. + - Publishing a maximum size message to the channel. + - Verifying that the message is received correctly using the callback method. + """ + listening_client, publishing_client = None, None + try: + channel = get_random_string(10) + message = "0" * 12 * 1024 * 1024 + + callback_messages: List[CoreCommands.PubSubMsg] = [] + callback, context = new_message, callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel}}, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub, timeout=10000 + ) + + result = await publishing_client.publish(message, channel) + if cluster_mode: + assert result == 1 + # allow the message to propagate + await asyncio.sleep(15) + + assert len(callback_messages) == 1 + + assert callback_messages[0].message == message.encode() + assert callback_messages[0].channel == channel.encode() + assert callback_messages[0].pattern is None + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.skip( + reason="This test requires special configuration for client-output-buffer-limit for valkey-server and timeouts seems to vary across platforms and server versions" + ) + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_pubsub_sharded_max_size_message_callback( + self, request, cluster_mode: bool + ): + """ + Tests publishing and receiving maximum size messages in sharded PUBSUB with callback method. + + This test verifies that very large messages (512MB - BulkString max size) can be published and received + correctly. It ensures that the PUBSUB system + can handle maximum size messages without errors and that the callback message + retrieval method works as expected. + + The test covers the following scenarios: + - Setting up PUBSUB subscription for a specific sharded channel with a callback. + - Publishing a maximum size message to the channel. + - Verifying that the message is received correctly using the callback method. + """ + publishing_client, listening_client = None, None + try: + channel = get_random_string(10) + message = "0" * 512 * 1024 * 1024 + + callback_messages: List[CoreCommands.PubSubMsg] = [] + callback, context = new_message, callback_messages + + pub_sub = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Sharded: {channel}}, + {}, + callback=callback, + context=context, + ) + + listening_client, publishing_client = await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub, timeout=10000 + ) + + # (Valkey version > 7) + if await check_if_server_version_lt(publishing_client, "7.0.0"): + pytest.skip("Valkey version required >= 7.0.0") + + assert ( + await cast(GlideClusterClient, publishing_client).publish( + message, channel, sharded=True + ) + == 1 + ) + + # allow the message to propagate + await asyncio.sleep(15) + + assert len(callback_messages) == 1 + + assert callback_messages[0].message == message.encode() + assert callback_messages[0].channel == channel.encode() + assert callback_messages[0].pattern is None + + finally: + await client_cleanup(listening_client, pub_sub if cluster_mode else None) + await client_cleanup(publishing_client, None) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_resp2_raise_an_error(self, request, cluster_mode: bool): + """Tests that when creating a resp2 client with PUBSUB - an error will be raised""" + channel = get_random_string(5) + + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel}}, + ) + + with pytest.raises(ConfigurationError): + await create_two_clients_with_pubsub( + request, cluster_mode, pub_sub_exact, protocol=ProtocolVersion.RESP2 + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_pubsub_context_with_no_callback_raise_error( + self, request, cluster_mode: bool + ): + """Tests that when creating a PUBSUB client in callback method with context but no callback raises an error""" + channel = get_random_string(5) + context: List[CoreCommands.PubSubMsg] = [] + pub_sub_exact = create_pubsub_subscription( + cluster_mode, + {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, + {GlideClientConfiguration.PubSubChannelModes.Exact: {channel}}, + context=context, + ) + + with pytest.raises(ConfigurationError): + await create_two_clients_with_pubsub(request, cluster_mode, pub_sub_exact) diff --git a/python/python/tests/test_scan.py b/python/python/tests/test_scan.py new file mode 100644 index 0000000000..907dc703d5 --- /dev/null +++ b/python/python/tests/test_scan.py @@ -0,0 +1,482 @@ +from __future__ import annotations + +from typing import List, cast + +import pytest +from glide import ClusterScanCursor +from glide.async_commands.command_args import ObjectType +from glide.config import ProtocolVersion +from glide.exceptions import RequestError +from glide.glide_client import GlideClient, GlideClusterClient +from tests.utils.utils import get_random_string + + +@pytest.mark.asyncio +class TestScan: + # Cluster scan tests + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_scan_simple(self, glide_client: GlideClusterClient): + key = get_random_string(10) + expected_keys = [f"{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + expected_keys_encoded = map(lambda k: k.encode(), expected_keys) + cursor = ClusterScanCursor() + keys: List[str] = [] + while not cursor.is_finished(): + result = await glide_client.scan(cursor) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = cast(List[str], result[1]) + keys.extend(result_keys) + + assert set(expected_keys_encoded) == set(keys) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_scan_with_object_type_and_pattern( + self, glide_client: GlideClusterClient + ): + key = get_random_string(10) + expected_keys = [f"key:{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + encoded_expected_keys = map(lambda k: k.encode(), expected_keys) + unexpected_type_keys = [f"{key}:{i}" for i in range(100, 200)] + for key in unexpected_type_keys: + await glide_client.sadd(key, ["value"]) + encoded_unexpected_type_keys = map(lambda k: k.encode(), unexpected_type_keys) + unexpected_pattern_keys = [f"{i}" for i in range(200, 300)] + await glide_client.mset({k: "value" for k in unexpected_pattern_keys}) + encoded_unexpected_pattern_keys = map( + lambda k: k.encode(), unexpected_pattern_keys + ) + keys: List[str] = [] + cursor = ClusterScanCursor() + while not cursor.is_finished(): + result = await glide_client.scan( + cursor, match=b"key:*", type=ObjectType.STRING + ) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = cast(List[str], result[1]) + keys.extend(result_keys) + + assert set(encoded_expected_keys) == set(keys) + assert not set(encoded_unexpected_type_keys).intersection(set(keys)) + assert not set(encoded_unexpected_pattern_keys).intersection(set(keys)) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_scan_with_count(self, glide_client: GlideClusterClient): + key = get_random_string(10) + expected_keys = [f"{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + encoded_expected_keys = map(lambda k: k.encode(), expected_keys) + cursor = ClusterScanCursor() + keys: List[str] = [] + successful_compared_scans = 0 + while not cursor.is_finished(): + result_of_1 = await glide_client.scan(cursor, count=1) + cursor = cast(ClusterScanCursor, result_of_1[0]) + result_keys_of_1 = cast(List[str], result_of_1[1]) + keys.extend(result_keys_of_1) + if cursor.is_finished(): + break + result_of_100 = await glide_client.scan(cursor, count=100) + cursor = cast(ClusterScanCursor, result_of_100[0]) + result_keys_of_100 = cast(List[str], result_of_100[1]) + keys.extend(result_keys_of_100) + if len(result_keys_of_100) > len(result_keys_of_1): + successful_compared_scans += 1 + + assert set(encoded_expected_keys) == set(keys) + assert successful_compared_scans > 0 + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_scan_with_match(self, glide_client: GlideClusterClient): + unexpected_keys = [f"{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in unexpected_keys}) + encoded_unexpected_keys = map(lambda k: k.encode(), unexpected_keys) + key = get_random_string(10) + expected_keys = [f"key:{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + encoded_expected_keys = map(lambda k: k.encode(), expected_keys) + cursor = ClusterScanCursor() + keys: List[str] = [] + while not cursor.is_finished(): + result = await glide_client.scan(cursor, match="key:*") + cursor = cast(ClusterScanCursor, result[0]) + result_keys = cast(List[str], result[1]) + keys.extend(result_keys) + assert set(encoded_expected_keys) == set(keys) + assert not set(encoded_unexpected_keys).intersection(set(keys)) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + # We test whether the cursor is cleaned up after it is deleted, which we expect to happen when th GC is called + async def test_cluster_scan_cleaning_cursor(self, glide_client: GlideClusterClient): + key = get_random_string(10) + await glide_client.mset( + {k: "value" for k in [f"{key}:{i}" for i in range(100)]} + ) + cursor = cast( + ClusterScanCursor, (await glide_client.scan(ClusterScanCursor()))[0] + ) + cursor_string = cursor.get_cursor() + del cursor + new_cursor_with_same_id = ClusterScanCursor(cursor_string) + with pytest.raises(RequestError) as e_info: + await glide_client.scan(new_cursor_with_same_id) + assert "Invalid scan_state_cursor id" in str(e_info.value) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_scan_all_types(self, glide_client: GlideClusterClient): + # We test that the scan command work for all types of keys + key = get_random_string(10) + string_keys = [f"{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in string_keys}) + encoded_string_keys = list(map(lambda k: k.encode(), string_keys)) + + set_key = get_random_string(10) + set_keys = [f"{set_key}:{i}" for i in range(100, 200)] + for key in set_keys: + await glide_client.sadd(key, ["value"]) + encoded_set_keys = list(map(lambda k: k.encode(), set_keys)) + + hash_key = get_random_string(10) + hash_keys = [f"{hash_key}:{i}" for i in range(200, 300)] + for key in hash_keys: + await glide_client.hset(key, {"field": "value"}) + encoded_hash_keys = list(map(lambda k: k.encode(), hash_keys)) + + list_key = get_random_string(10) + list_keys = [f"{list_key}:{i}" for i in range(300, 400)] + for key in list_keys: + await glide_client.lpush(key, ["value"]) + encoded_list_keys = list(map(lambda k: k.encode(), list_keys)) + + zset_key = get_random_string(10) + zset_keys = [f"{zset_key}:{i}" for i in range(400, 500)] + for key in zset_keys: + await glide_client.zadd(key, {"value": 1}) + encoded_zset_keys = list(map(lambda k: k.encode(), zset_keys)) + + stream_key = get_random_string(10) + stream_keys = [f"{stream_key}:{i}" for i in range(500, 600)] + for key in stream_keys: + await glide_client.xadd(key, [("field", "value")]) + encoded_stream_keys = list(map(lambda k: k.encode(), stream_keys)) + + cursor = ClusterScanCursor() + keys: List[bytes] = [] + while not cursor.is_finished(): + result = await glide_client.scan(cursor, type=ObjectType.STRING) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = result[1] + keys.extend(cast(List[bytes], result_keys)) + assert set(encoded_string_keys) == set(keys) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + cursor = ClusterScanCursor() + keys.clear() + while not cursor.is_finished(): + result = await glide_client.scan(cursor, type=ObjectType.SET) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = result[1] + keys.extend(cast(List[bytes], result_keys)) + assert set(encoded_set_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + cursor = ClusterScanCursor() + keys.clear() + while not cursor.is_finished(): + result = await glide_client.scan(cursor, type=ObjectType.HASH) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = result[1] + keys.extend(cast(List[bytes], result_keys)) + assert set(encoded_hash_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + cursor = ClusterScanCursor() + keys.clear() + while not cursor.is_finished(): + result = await glide_client.scan(cursor, type=ObjectType.LIST) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = result[1] + keys.extend(cast(List[bytes], result_keys)) + assert set(encoded_list_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + cursor = ClusterScanCursor() + keys.clear() + while not cursor.is_finished(): + result = await glide_client.scan(cursor, type=ObjectType.ZSET) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = result[1] + keys.extend(cast(List[bytes], result_keys)) + assert set(encoded_zset_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + cursor = ClusterScanCursor() + keys.clear() + while not cursor.is_finished(): + result = await glide_client.scan(cursor, type=ObjectType.STREAM) + cursor = cast(ClusterScanCursor, result[0]) + result_keys = result[1] + keys.extend(cast(List[bytes], result_keys)) + assert set(encoded_stream_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + + # Standalone scan tests + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_scan_simple(self, glide_client: GlideClient): + key = get_random_string(10) + expected_keys = [f"{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + encoded_expected_keys = map(lambda k: k.encode(), expected_keys) + keys: List[str] = [] + cursor = b"0" + while True: + result = await glide_client.scan(cursor) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes + new_keys = cast(List[str], result[1]) + keys.extend(new_keys) + if cursor == b"0": + break + assert set(encoded_expected_keys) == set(keys) + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_scan_with_object_type_and_pattern( + self, glide_client: GlideClient + ): + key = get_random_string(10) + expected_keys = [f"key:{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + unexpected_type_keys = [f"key:{i}" for i in range(100, 200)] + for key in unexpected_type_keys: + await glide_client.sadd(key, ["value"]) + unexpected_pattern_keys = [f"{i}" for i in range(200, 300)] + for key in unexpected_pattern_keys: + await glide_client.set(key, "value") + keys: List[str] = [] + cursor = b"0" + while True: + result = await glide_client.scan( + cursor, match=b"key:*", type=ObjectType.STRING + ) + cursor = cast(bytes, result[0]) + keys.extend(list(map(lambda k: k.decode(), cast(List[bytes], result[1])))) + if cursor == b"0": + break + assert set(expected_keys) == set(keys) + assert not set(unexpected_type_keys).intersection(set(keys)) + assert not set(unexpected_pattern_keys).intersection(set(keys)) + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_scan_with_count(self, glide_client: GlideClient): + key = get_random_string(10) + expected_keys = [f"{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + encoded_expected_keys = map(lambda k: k.encode(), expected_keys) + cursor = "0" + keys: List[str] = [] + successful_compared_scans = 0 + while True: + result_of_1 = await glide_client.scan(cursor, count=1) + cursor_bytes = cast(bytes, result_of_1[0]) + cursor = cursor_bytes.decode() + keys_of_1 = cast(List[str], result_of_1[1]) + keys.extend(keys_of_1) + result_of_100 = await glide_client.scan(cursor, count=100) + cursor_bytes = cast(bytes, result_of_100[0]) + cursor = cursor_bytes.decode() + keys_of_100 = cast(List[str], result_of_100[1]) + keys.extend(keys_of_100) + if len(keys_of_100) > len(keys_of_1): + successful_compared_scans += 1 + if cursor == "0": + break + assert set(encoded_expected_keys) == set(keys) + assert successful_compared_scans > 0 + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_scan_with_match(self, glide_client: GlideClient): + key = get_random_string(10) + expected_keys = [f"key:{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in expected_keys}) + encoded_expected_keys = map(lambda k: k.encode(), expected_keys) + unexpected_keys = [f"{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in [f"{i}" for i in range(100)]}) + encoded_unexpected_keys = map(lambda k: k.encode(), unexpected_keys) + cursor = "0" + keys: List[str] = [] + while True: + result = await glide_client.scan(cursor, match="key:*") + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = cast(List[str], result[1]) + keys.extend(new_keys) + if cursor == "0": + break + assert set(encoded_expected_keys) == set(keys) + assert not set(encoded_unexpected_keys).intersection(set(keys)) + + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_scan_all_types(self, glide_client: GlideClient): + # We test that the scan command work for all types of keys + key = get_random_string(10) + string_keys = [f"{key}:{i}" for i in range(100)] + await glide_client.mset({k: "value" for k in string_keys}) + encoded_string_keys = list(map(lambda k: k.encode(), string_keys)) + + set_keys = [f"{key}:{i}" for i in range(100, 200)] + for key in set_keys: + await glide_client.sadd(key, ["value"]) + encoded_set_keys = list(map(lambda k: k.encode(), set_keys)) + + hash_keys = [f"{key}:{i}" for i in range(200, 300)] + for key in hash_keys: + await glide_client.hset(key, {"field": "value"}) + encoded_hash_keys = list(map(lambda k: k.encode(), hash_keys)) + + list_keys = [f"{key}:{i}" for i in range(300, 400)] + for key in list_keys: + await glide_client.lpush(key, ["value"]) + encoded_list_keys = list(map(lambda k: k.encode(), list_keys)) + + zset_keys = [f"{key}:{i}" for i in range(400, 500)] + for key in zset_keys: + await glide_client.zadd(key, {"value": 1}) + encoded_zset_keys = list(map(lambda k: k.encode(), zset_keys)) + + stream_keys = [f"{key}:{i}" for i in range(500, 600)] + for key in stream_keys: + await glide_client.xadd(key, [("field", "value")]) + encoded_stream_keys = list(map(lambda k: k.encode(), stream_keys)) + + cursor = "0" + keys: List[bytes] = [] + while True: + result = await glide_client.scan(cursor, type=ObjectType.STRING) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = result[1] + keys.extend(cast(List[bytes], new_keys)) + if cursor == "0": + break + assert set(encoded_string_keys) == set(keys) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + keys.clear() + while True: + result = await glide_client.scan(cursor, type=ObjectType.SET) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = result[1] + keys.extend(cast(List[bytes], new_keys)) + if cursor == "0": + break + assert set(encoded_set_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + keys.clear() + while True: + result = await glide_client.scan(cursor, type=ObjectType.HASH) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = result[1] + keys.extend(cast(List[bytes], new_keys)) + if cursor == "0": + break + assert set(encoded_hash_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + keys.clear() + while True: + result = await glide_client.scan(cursor, type=ObjectType.LIST) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = result[1] + keys.extend(cast(List[bytes], new_keys)) + if cursor == "0": + break + assert set(encoded_list_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + keys.clear() + while True: + result = await glide_client.scan(cursor, type=ObjectType.ZSET) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = result[1] + keys.extend(cast(List[bytes], new_keys)) + if cursor == "0": + break + assert set(encoded_zset_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_stream_keys).intersection(set(keys)) + + keys.clear() + while True: + result = await glide_client.scan(cursor, type=ObjectType.STREAM) + cursor_bytes = cast(bytes, result[0]) + cursor = cursor_bytes.decode() + new_keys = result[1] + keys.extend(cast(List[bytes], new_keys)) + if cursor == "0": + break + assert set(encoded_stream_keys) == set(keys) + assert not set(encoded_string_keys).intersection(set(keys)) + assert not set(encoded_set_keys).intersection(set(keys)) + assert not set(encoded_hash_keys).intersection(set(keys)) + assert not set(encoded_list_keys).intersection(set(keys)) + assert not set(encoded_zset_keys).intersection(set(keys)) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 0f89cfe6fe..2b1293b943 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -1,16 +1,52 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from datetime import datetime, timezone -from typing import List, Union +import time +from datetime import date, datetime, timedelta, timezone +from typing import List, Optional, Union, cast import pytest from glide import RequestError -from glide.async_commands.core import GeospatialData, InsertPosition +from glide.async_commands.bitmap import ( + BitFieldGet, + BitFieldSet, + BitmapIndexType, + BitOffset, + BitOffsetMultiplier, + BitwiseOperation, + OffsetOptions, + SignedEncoding, + UnsignedEncoding, +) +from glide.async_commands.command_args import Limit, ListDirection, OrderBy +from glide.async_commands.core import ( + ExpiryGetEx, + ExpiryTypeGetEx, + FlushMode, + FunctionRestorePolicy, + InsertPosition, +) from glide.async_commands.sorted_set import ( + AggregationType, + GeoSearchByBox, + GeoSearchByRadius, + GeospatialData, + GeoUnit, InfBound, LexBoundary, + OrderBy, RangeByIndex, ScoreBoundary, + ScoreFilter, +) +from glide.async_commands.stream import ( + IdBound, + MaxId, + MinId, + StreamAddOptions, + StreamClaimOptions, + StreamGroupOptions, + StreamReadGroupOptions, + TrimByMinId, ) from glide.async_commands.transaction import ( BaseTransaction, @@ -18,66 +54,197 @@ Transaction, ) from glide.config import ProtocolVersion -from glide.constants import OK, TResult -from glide.redis_client import RedisClient, RedisClusterClient, TRedisClient +from glide.constants import OK, TResult, TSingleNodeRoute +from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient +from glide.routes import SlotIdRoute, SlotType from tests.conftest import create_client -from tests.test_async_client import check_if_server_version_lt, get_random_string +from tests.utils.utils import ( + check_if_server_version_lt, + convert_bytes_to_string_object, + generate_lua_lib_code, + get_random_string, +) async def transaction_test( transaction: Union[Transaction, ClusterTransaction], keyslot: str, - redis_client: TRedisClient, + glide_client: TGlideClient, ) -> List[TResult]: - key = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot - key2 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot - key3 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key4 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key5 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key6 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key7 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key8 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key9 = "{{{}}}:{}".format(keyslot, get_random_string(3)) - key10 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # list + key = "{{{}}}:{}".format(keyslot, get_random_string(10)) # to get the same slot + key2 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # to get the same slot + key3 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + key4 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + key5 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + key6 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + key7 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + key8 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + key9 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # list + key10 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # hyper log log + key11 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # streams + key12 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # geo + key13 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # sorted set + key14 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # sorted set + key15 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # sorted set + key16 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # sorted set + key17 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # sort + key18 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # sort + key19 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # bitmap + key20 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # bitmap + key22 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # getex + key23 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # string + key24 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # string + key25 = "{{{}}}:{}".format(keyslot, get_random_string(10)) # list value = datetime.now(timezone.utc).strftime("%m/%d/%Y, %H:%M:%S") + value_bytes = value.encode() value2 = get_random_string(5) + value2_bytes = value2.encode() + value3 = get_random_string(5) + value3_bytes = value3.encode() + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) args: List[TResult] = [] + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.function_load(code) + args.append(lib_name.encode()) + transaction.function_load(code, True) + args.append(lib_name.encode()) + transaction.function_list(lib_name) + args.append( + [ + { + b"library_name": lib_name.encode(), + b"engine": b"LUA", + b"functions": [ + { + b"name": func_name.encode(), + b"description": None, + b"flags": {b"no-writes"}, + } + ], + } + ] + ) + transaction.function_list(lib_name, True) + args.append( + [ + { + b"library_name": lib_name.encode(), + b"engine": b"LUA", + b"functions": [ + { + b"name": func_name.encode(), + b"description": None, + b"flags": {b"no-writes"}, + } + ], + b"library_code": code.encode(), + } + ] + ) + transaction.fcall(func_name, [], arguments=["one", "two"]) + args.append(b"one") + transaction.fcall(func_name, [key], arguments=["one", "two"]) + args.append(b"one") + transaction.fcall_ro(func_name, [], arguments=["one", "two"]) + args.append(b"one") + transaction.fcall_ro(func_name, [key], arguments=["one", "two"]) + args.append(b"one") + transaction.function_delete(lib_name) + args.append(OK) + transaction.function_flush() + args.append(OK) + transaction.function_flush(FlushMode.ASYNC) + args.append(OK) + transaction.function_flush(FlushMode.SYNC) + args.append(OK) + transaction.function_stats() + args.append( + { + b"running_script": None, + b"engines": { + b"LUA": { + b"libraries_count": 0, + b"functions_count": 0, + } + }, + } + ) + transaction.dbsize() args.append(0) transaction.set(key, value) args.append(OK) + transaction.setrange(key, 0, value) + args.append(len(value)) transaction.get(key) - args.append(value) + args.append(value_bytes) + transaction.get(key.encode()) + args.append(value_bytes) transaction.type(key) - args.append("string") + args.append(b"string") + transaction.type(key.encode()) + args.append(b"string") transaction.echo(value) - args.append(value) + args.append(value_bytes) + transaction.echo(value.encode()) + args.append(value_bytes) transaction.strlen(key) args.append(len(value)) + transaction.strlen(key.encode()) + args.append(len(value)) transaction.append(key, value) args.append(len(value) * 2) transaction.persist(key) args.append(False) + transaction.ttl(key) + args.append(-1) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.expiretime(key) + args.append(-1) + transaction.pexpiretime(key) + args.append(-1) + + if not await check_if_server_version_lt(glide_client, "6.2.0"): + transaction.copy(key, key2, replace=True) + args.append(True) transaction.rename(key, key2) args.append(OK) transaction.exists([key2]) args.append(1) + transaction.touch([key2]) + args.append(1) transaction.delete([key2]) args.append(1) transaction.get(key2) args.append(None) + transaction.set(key, value) + args.append(OK) + transaction.getrange(key, 0, -1) + args.append(value_bytes) + transaction.getdel(key) + args.append(value_bytes) + transaction.getdel(key) + args.append(None) + transaction.mset({key: value, key2: value2}) args.append(OK) + transaction.msetnx({key: value, key2: value2}) + args.append(False) transaction.mget([key, key2]) - args.append([value, value2]) + args.append([value_bytes, value2_bytes]) + + transaction.renamenx(key, key2) + args.append(False) transaction.incr(key3) args.append(1) @@ -96,23 +263,23 @@ async def transaction_test( args.append(1) transaction.ping() - args.append("PONG") + args.append(b"PONG") transaction.config_set({"timeout": "1000"}) args.append(OK) transaction.config_get(["timeout"]) - args.append({"timeout": "1000"}) + args.append({b"timeout": b"1000"}) transaction.hset(key4, {key: value, key2: value2}) args.append(2) transaction.hget(key4, key2) - args.append(value2) + args.append(value2_bytes) transaction.hlen(key4) args.append(2) transaction.hvals(key4) - args.append([value, value2]) + args.append([value_bytes, value2_bytes]) transaction.hkeys(key4) - args.append([key, key2]) + args.append([key.encode(), key2.encode()]) transaction.hsetnx(key4, key, value) args.append(False) transaction.hincrby(key4, key3, 5) @@ -123,11 +290,30 @@ async def transaction_test( transaction.hexists(key4, key) args.append(True) transaction.hmget(key4, [key, "nonExistingField", key2]) - args.append([value, None, value2]) + args.append([value_bytes, None, value2_bytes]) transaction.hgetall(key4) - args.append({key: value, key2: value2, key3: "10.5"}) + key3_bytes = key3.encode() + args.append( + { + key.encode(): value_bytes, + key2.encode(): value2_bytes, + key3_bytes: b"10.5", + } + ) transaction.hdel(key4, [key, key2]) args.append(2) + transaction.hscan(key4, "0") + args.append([b"0", [key3.encode(), b"10.5"]]) + transaction.hscan(key4, "0", match="*", count=10) + args.append([b"0", [key3.encode(), b"10.5"]]) + transaction.hrandfield(key4) + args.append(key3_bytes) + transaction.hrandfield_count(key4, 1) + args.append([key3_bytes]) + transaction.hrandfield_withvalues(key4, 1) + args.append([[key3_bytes, b"10.5"]]) + transaction.hstrlen(key4, key3) + args.append(4) transaction.client_getname() args.append(None) @@ -137,58 +323,108 @@ async def transaction_test( transaction.llen(key5) args.append(4) transaction.lindex(key5, 0) - args.append(value2) + args.append(value2_bytes) transaction.lpop(key5) - args.append(value2) + args.append(value2_bytes) transaction.lrem(key5, 1, value) args.append(1) transaction.ltrim(key5, 0, 1) args.append(OK) transaction.lrange(key5, 0, -1) - args.append([value2, value]) + args.append([value2_bytes, value_bytes]) + transaction.lmove(key5, key6, ListDirection.LEFT, ListDirection.LEFT) + args.append(value2_bytes) + transaction.blmove(key6, key5, ListDirection.LEFT, ListDirection.LEFT, 1) + args.append(value2_bytes) transaction.lpop_count(key5, 2) - args.append([value2, value]) + args.append([value2_bytes, value_bytes]) transaction.linsert(key5, InsertPosition.BEFORE, "non_existing_pivot", "element") args.append(0) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.lpush(key5, [value, value2]) + args.append(2) + transaction.lmpop([key5], ListDirection.LEFT) + args.append({key5.encode(): [value2_bytes]}) + transaction.blmpop([key5], ListDirection.LEFT, 0.1) + args.append({key5.encode(): [value_bytes]}) transaction.rpush(key6, [value, value2, value2]) args.append(3) transaction.rpop(key6) - args.append(value2) + args.append(value2_bytes) transaction.rpop_count(key6, 2) - args.append([value2, value]) + args.append([value2_bytes, value_bytes]) transaction.rpushx(key9, ["_"]) args.append(0) transaction.lpushx(key9, ["_"]) args.append(0) + transaction.lpush(key9, [value, value2, value3]) + args.append(3) + transaction.blpop([key9], 1) + args.append([key9.encode(), value3_bytes]) + transaction.brpop([key9], 1) + args.append([key9.encode(), value_bytes]) + transaction.lset(key9, 0, value2) + args.append(OK) transaction.sadd(key7, ["foo", "bar"]) args.append(2) + transaction.smismember(key7, ["foo", "baz"]) + args.append([True, False]) + transaction.sdiffstore(key7, [key7]) + args.append(2) transaction.srem(key7, ["foo"]) args.append(1) + transaction.sscan(key7, "0") + args.append([b"0", [b"bar"]]) + transaction.sscan(key7, "0", match="*", count=10) + args.append([b"0", [b"bar"]]) transaction.smembers(key7) - args.append({"bar"}) + args.append({b"bar"}) transaction.scard(key7) args.append(1) transaction.sismember(key7, "bar") args.append(True) transaction.spop(key7) - args.append("bar") + args.append(b"bar") transaction.sadd(key7, ["foo", "bar"]) args.append(2) + transaction.sunionstore(key7, [key7, key7]) + args.append(2) + transaction.sinter([key7, key7]) + args.append({b"foo", b"bar"}) + transaction.sunion([key7, key7]) + args.append({b"foo", b"bar"}) + transaction.sinterstore(key7, [key7, key7]) + args.append(2) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.sintercard([key7, key7]) + args.append(2) + transaction.sintercard([key7, key7], 1) + args.append(1) + transaction.sdiff([key7, key7]) + args.append(set()) transaction.spop_count(key7, 4) - args.append({"foo", "bar"}) + args.append({b"foo", b"bar"}) + transaction.smove(key7, key7, "non_existing_member") + args.append(False) transaction.zadd(key8, {"one": 1, "two": 2, "three": 3, "four": 4}) - args.append(4) + args.append(4.0) transaction.zrank(key8, "one") args.append(0) - if not await check_if_server_version_lt(redis_client, "7.2.0"): + transaction.zrevrank(key8, "one") + args.append(3) + if not await check_if_server_version_lt(glide_client, "7.2.0"): transaction.zrank_withscore(key8, "one") args.append([0, 1]) + transaction.zrevrank_withscore(key8, "one") + args.append([3, 1]) transaction.zadd_incr(key8, "one", 3) - args.append(4) + args.append(4.0) + transaction.zincrby(key8, 3, "one") + args.append(7.0) transaction.zrem(key8, ["one"]) args.append(1) transaction.zcard(key8) @@ -200,129 +436,468 @@ async def transaction_test( transaction.zscore(key8, "two") args.append(2.0) transaction.zrange(key8, RangeByIndex(start=0, stop=-1)) - args.append(["two", "three", "four"]) + args.append([b"two", b"three", b"four"]) transaction.zrange_withscores(key8, RangeByIndex(start=0, stop=-1)) - args.append({"two": 2, "three": 3, "four": 4}) - transaction.zpopmin(key8) - args.append({"two": 2.0}) + args.append({b"two": 2.0, b"three": 3.0, b"four": 4.0}) + transaction.zmscore(key8, ["two", "three"]) + args.append([2.0, 3.0]) + transaction.zrangestore(key8, key8, RangeByIndex(0, -1)) + args.append(3) + transaction.bzpopmin([key8], 0.5) + args.append([key8.encode(), b"two", 2.0]) + transaction.bzpopmax([key8], 0.5) + args.append([key8.encode(), b"four", 4.0]) + # key8 now only contains one member ("three") + transaction.zrandmember(key8) + args.append(b"three") + transaction.zrandmember_count(key8, 1) + args.append([b"three"]) + transaction.zrandmember_withscores(key8, 1) + args.append([[b"three", 3.0]]) + transaction.zscan(key8, "0") + args.append([b"0", [b"three", b"3"]]) + transaction.zscan(key8, "0", match="*", count=20) + args.append([b"0", [b"three", b"3"]]) transaction.zpopmax(key8) - args.append({"four": 4}) + args.append({b"three": 3.0}) + transaction.zpopmin(key8) + args.append({}) # type: ignore transaction.zremrangebyscore(key8, InfBound.NEG_INF, InfBound.POS_INF) - args.append(1) + args.append(0) transaction.zremrangebylex(key8, InfBound.NEG_INF, InfBound.POS_INF) args.append(0) + transaction.zremrangebyrank(key8, 0, 10) + args.append(0) + transaction.zdiffstore(key8, [key8, key8]) + args.append(0) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.zmpop([key8], ScoreFilter.MAX) + args.append(None) + transaction.zmpop([key8], ScoreFilter.MAX, 1) + args.append(None) + + transaction.zadd(key13, {"one": 1.0, "two": 2.0}) + args.append(2) + transaction.zdiff([key13, key8]) + args.append([b"one", b"two"]) + transaction.zdiff_withscores([key13, key8]) + args.append({b"one": 1.0, b"two": 2.0}) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.zintercard([key13, key8]) + args.append(0) + transaction.zintercard([key13, key8], 1) + args.append(0) + + transaction.zadd(key14, {"one": 1, "two": 2}) + args.append(2) + transaction.zadd(key15, {"one": 1.0, "two": 2.0, "three": 3.5}) + args.append(3) + transaction.zinter([key14, key15]) + args.append([b"one", b"two"]) + transaction.zinter_withscores(cast(List[Union[str, bytes]], [key14, key15])) + args.append({b"one": 2.0, b"two": 4.0}) + transaction.zinterstore(key8, cast(List[Union[str, bytes]], [key14, key15])) + args.append(2) + transaction.zunion([key14, key15]) + args.append([b"one", b"three", b"two"]) + transaction.zunion_withscores(cast(List[Union[str, bytes]], [key14, key15])) + args.append({b"one": 2.0, b"two": 4.0, b"three": 3.5}) + transaction.zunionstore( + key8, cast(List[Union[str, bytes]], [key14, key15]), AggregationType.MAX + ) + args.append(3) + + transaction.pfadd(key10, ["a", "b", "c"]) + args.append(1) + transaction.pfmerge(key10, []) + args.append(OK) + transaction.pfcount([key10]) + args.append(3) + + transaction.setbit(key19, 1, 1) + args.append(0) + transaction.getbit(key19, 1) + args.append(1) + + transaction.set(key20, "foobar") + args.append(OK) + transaction.bitcount(key20) + args.append(26) + transaction.bitcount(key20, OffsetOptions(1, 1)) + args.append(6) + transaction.bitpos(key20, 1) + args.append(1) + + if not await check_if_server_version_lt(glide_client, "6.0.0"): + transaction.bitfield_read_only( + key20, [BitFieldGet(SignedEncoding(5), BitOffset(3))] + ) + args.append([6]) + + transaction.set(key19, "abcdef") + args.append(OK) + transaction.bitop(BitwiseOperation.AND, key19, [key19, key20]) + args.append(6) + transaction.get(key19) + args.append(b"`bc`ab") + transaction.bitfield( + key20, [BitFieldSet(UnsignedEncoding(10), BitOffsetMultiplier(3), 4)] + ) + args.append([609]) + + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.set(key20, "foobar") + args.append(OK) + transaction.bitcount(key20, OffsetOptions(5, 30, BitmapIndexType.BIT)) + args.append(17) + transaction.bitpos_interval(key20, 1, 44, 50, BitmapIndexType.BIT) + args.append(46) transaction.geoadd( - key9, + key12, { "Palermo": GeospatialData(13.361389, 38.115556), "Catania": GeospatialData(15.087269, 37.502669), }, ) args.append(2) - transaction.geohash(key9, ["Palermo", "Catania", "Place"]) - args.append(["sqc8b49rny0", "sqdtr74hyu0", None]) + transaction.geodist(key12, "Palermo", "Catania") + args.append(166274.1516) + transaction.geohash(key12, ["Palermo", "Catania", "Place"]) + args.append([b"sqc8b49rny0", b"sqdtr74hyu0", None]) + transaction.geopos(key12, ["Palermo", "Catania", "Place"]) + # The comparison allows for a small tolerance level due to potential precision errors in floating-point calculations + # No worries, Python can handle it, therefore, this shouldn't fail + args.append( + [ + [13.36138933897018433, 38.11555639549629859], + [15.08726745843887329, 37.50266842333162032], + None, + ] + ) + + transaction.geosearch( + key12, "Catania", GeoSearchByRadius(200, GeoUnit.KILOMETERS), OrderBy.ASC + ) + args.append([b"Catania", b"Palermo"]) + transaction.geosearchstore( + key12, + key12, + GeospatialData(15, 37), + GeoSearchByBox(400, 400, GeoUnit.KILOMETERS), + store_dist=True, + ) + args.append(2) + + transaction.xadd(key11, [("foo", "bar")], StreamAddOptions(id="0-1")) + args.append(b"0-1") + transaction.xadd(key11, [("foo", "bar")], StreamAddOptions(id="0-2")) + args.append(b"0-2") + transaction.xadd(key11, [("foo", "bar")], StreamAddOptions(id="0-3")) + args.append(b"0-3") + transaction.xlen(key11) + args.append(3) + transaction.xread({key11: "0-2"}) + args.append({key11.encode(): {b"0-3": [[b"foo", b"bar"]]}}) + transaction.xrange(key11, IdBound("0-1"), IdBound("0-1")) + args.append({b"0-1": [[b"foo", b"bar"]]}) + transaction.xrevrange(key11, IdBound("0-1"), IdBound("0-1")) + args.append({b"0-1": [[b"foo", b"bar"]]}) + transaction.xtrim(key11, TrimByMinId(threshold="0-2", exact=True)) + args.append(1) + transaction.xinfo_groups(key11) + args.append([]) + + group_name1 = get_random_string(10) + group_name2 = get_random_string(10) + consumer = get_random_string(10) + consumer2 = get_random_string(10) + transaction.xgroup_create(key11, group_name1, "0-2") + args.append(OK) + transaction.xgroup_create( + key11, group_name2, "0-0", StreamGroupOptions(make_stream=True) + ) + args.append(OK) + transaction.xinfo_consumers(key11, group_name1) + args.append([]) + transaction.xgroup_create_consumer(key11, group_name1, consumer) + args.append(True) + transaction.xgroup_set_id(key11, group_name1, "0-2") + args.append(OK) + transaction.xreadgroup({key11: ">"}, group_name1, consumer) + args.append({key11.encode(): {b"0-3": [[b"foo", b"bar"]]}}) + transaction.xreadgroup( + {key11: "0-3"}, group_name1, consumer, StreamReadGroupOptions(count=2) + ) + args.append({key11.encode(): {}}) + transaction.xclaim(key11, group_name1, consumer, 0, ["0-1"]) + args.append({}) + transaction.xclaim( + key11, group_name1, consumer, 0, ["0-3"], StreamClaimOptions(is_force=True) + ) + args.append({b"0-3": [[b"foo", b"bar"]]}) + transaction.xclaim_just_id(key11, group_name1, consumer, 0, ["0-3"]) + args.append([b"0-3"]) + transaction.xclaim_just_id( + key11, group_name1, consumer, 0, ["0-4"], StreamClaimOptions(is_force=True) + ) + args.append([]) + + transaction.xpending(key11, group_name1) + args.append([1, b"0-3", b"0-3", [[consumer.encode(), b"1"]]]) + + min_version = "6.2.0" + if not await check_if_server_version_lt(glide_client, min_version): + transaction.xautoclaim(key11, group_name1, consumer, 0, "0-0") + transaction.xautoclaim_just_id(key11, group_name1, consumer, 0, "0-0") + # if using Valkey 7.0.0 or above, responses also include a list of entry IDs that were removed from the Pending + # Entries List because they no longer exist in the stream + if await check_if_server_version_lt(glide_client, "7.0.0"): + args.append( + [b"0-0", {b"0-3": [[b"foo", b"bar"]]}] + ) # transaction.xautoclaim(key11, group_name1, consumer, 0, "0-0") + args.append( + [b"0-0", [b"0-3"]] + ) # transaction.xautoclaim_just_id(key11, group_name1, consumer, 0, "0-0") + else: + args.append( + [b"0-0", {b"0-3": [[b"foo", b"bar"]]}, []] + ) # transaction.xautoclaim(key11, group_name1, consumer, 0, "0-0") + args.append( + [b"0-0", [b"0-3"], []] + ) # transaction.xautoclaim_just_id(key11, group_name1, consumer, 0, "0-0") + + transaction.xack(key11, group_name1, ["0-3"]) + args.append(1) + transaction.xpending_range(key11, group_name1, MinId(), MaxId(), 1) + args.append([]) + transaction.xgroup_del_consumer(key11, group_name1, consumer) + args.append(0) + transaction.xgroup_destroy(key11, group_name1) + args.append(True) + transaction.xgroup_destroy(key11, group_name2) + args.append(True) + + transaction.xdel(key11, ["0-3", "0-5"]) + args.append(1) + + transaction.lpush(key17, ["2", "1", "4", "3", "a"]) + args.append(5) + transaction.sort( + key17, + limit=Limit(1, 4), + order=OrderBy.ASC, + alpha=True, + ) + args.append([b"2", b"3", b"4", b"a"]) + if not await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.sort_ro( + key17, + limit=Limit(1, 4), + order=OrderBy.ASC, + alpha=True, + ) + args.append([b"2", b"3", b"4", b"a"]) + transaction.sort_store( + key17, + key18, + limit=Limit(1, 4), + order=OrderBy.ASC, + alpha=True, + ) + args.append(4) + transaction.sadd(key7, ["one"]) + args.append(1) + transaction.srandmember(key7) + args.append(b"one") + transaction.srandmember_count(key7, 1) + args.append([b"one"]) + transaction.flushall(FlushMode.ASYNC) + args.append(OK) + transaction.flushall() + args.append(OK) + transaction.flushdb(FlushMode.ASYNC) + args.append(OK) + transaction.flushdb() + args.append(OK) + transaction.set(key, "foo") + args.append(OK) + transaction.random_key() + args.append(key.encode()) + + min_version = "6.0.6" + if not await check_if_server_version_lt(glide_client, min_version): + transaction.rpush(key25, ["a", "a", "b", "c", "a", "b"]) + args.append(6) + transaction.lpos(key25, "a") + args.append(0) + transaction.lpos(key25, "a", 1, 0, 0) + args.append([0, 1, 4]) + + min_version = "6.2.0" + if not await check_if_server_version_lt(glide_client, min_version): + transaction.flushall(FlushMode.SYNC) + args.append(OK) + transaction.flushdb(FlushMode.SYNC) + args.append(OK) + + min_version = "6.2.0" + if not await check_if_server_version_lt(glide_client, min_version): + transaction.set(key22, "value") + args.append(OK) + transaction.getex(key22) + args.append(b"value") + transaction.getex(key22, ExpiryGetEx(ExpiryTypeGetEx.SEC, 1)) + args.append(b"value") + + min_version = "7.0.0" + if not await check_if_server_version_lt(glide_client, min_version): + transaction.zadd(key16, {"a": 1, "b": 2, "c": 3, "d": 4}) + args.append(4) + transaction.bzmpop([key16], ScoreFilter.MAX, 0.1) + args.append([key16.encode(), {b"d": 4.0}]) + transaction.bzmpop([key16], ScoreFilter.MIN, 0.1, 2) + args.append([key16.encode(), {b"a": 1.0, b"b": 2.0}]) + + transaction.mset({key23: "abcd1234", key24: "bcdef1234"}) + args.append(OK) + transaction.lcs(key23, key24) + args.append(b"bcd1234") + transaction.lcs_len(key23, key24) + args.append(7) + transaction.lcs_idx(key23, key24) + args.append({b"matches": [[[4, 7], [5, 8]], [[1, 3], [0, 2]]], b"len": 7}) + transaction.lcs_idx(key23, key24, min_match_len=4) + args.append({b"matches": [[[4, 7], [5, 8]]], b"len": 7}) + transaction.lcs_idx(key23, key24, with_match_len=True) + args.append({b"matches": [[[4, 7], [5, 8], 4], [[1, 3], [0, 2], 3]], b"len": 7}) + return args @pytest.mark.asyncio class TestTransaction: + + async def exec_transaction( + self, + glide_client: TGlideClient, + transaction: BaseTransaction, + route: Optional[TSingleNodeRoute] = None, + ) -> Optional[List[TResult]]: + """ + Exec a transaction on a client with proper typing. Casts are required to satisfy `mypy`. + """ + if isinstance(glide_client, GlideClient): + return await cast(GlideClient, glide_client).exec( + cast(Transaction, transaction) + ) + else: + return await cast(GlideClusterClient, glide_client).exec( + cast(ClusterTransaction, transaction), route + ) + @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_transaction_with_different_slots(self, redis_client: TRedisClient): - transaction = ( - Transaction() - if isinstance(redis_client, RedisClient) - else ClusterTransaction() - ) + async def test_transaction_with_different_slots( + self, glide_client: GlideClusterClient + ): + transaction = ClusterTransaction() transaction.set("key1", "value1") transaction.set("key2", "value2") with pytest.raises(RequestError, match="CrossSlot"): - await redis_client.exec(transaction) + await glide_client.exec(transaction) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_transaction_custom_command(self, redis_client: TRedisClient): + async def test_transaction_custom_command(self, glide_client: TGlideClient): key = get_random_string(10) transaction = ( Transaction() - if isinstance(redis_client, RedisClient) + if isinstance(glide_client, GlideClient) else ClusterTransaction() ) transaction.custom_command(["HSET", key, "foo", "bar"]) transaction.custom_command(["HGET", key, "foo"]) - result = await redis_client.exec(transaction) - assert result == [1, "bar"] + result = await self.exec_transaction(glide_client, transaction) + assert result == [1, b"bar"] @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_transaction_custom_unsupported_command( - self, redis_client: TRedisClient + self, glide_client: TGlideClient ): key = get_random_string(10) transaction = ( Transaction() - if isinstance(redis_client, RedisClient) + if isinstance(glide_client, GlideClient) else ClusterTransaction() ) transaction.custom_command(["WATCH", key]) with pytest.raises(RequestError) as e: - await redis_client.exec(transaction) + await self.exec_transaction(glide_client, transaction) assert "WATCH inside MULTI is not allowed" in str( e ) # TODO : add an assert on EXEC ABORT @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_transaction_discard_command(self, redis_client: TRedisClient): + async def test_transaction_discard_command(self, glide_client: TGlideClient): key = get_random_string(10) - await redis_client.set(key, "1") + await glide_client.set(key, "1") transaction = ( Transaction() - if isinstance(redis_client, RedisClient) + if isinstance(glide_client, GlideClient) else ClusterTransaction() ) transaction.custom_command(["INCR", key]) transaction.custom_command(["DISCARD"]) with pytest.raises(RequestError) as e: - await redis_client.exec(transaction) + await self.exec_transaction(glide_client, transaction) assert "EXEC without MULTI" in str(e) # TODO : add an assert on EXEC ABORT - value = await redis_client.get(key) - assert value == "1" + value = await glide_client.get(key) + assert value == b"1" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_transaction_exec_abort(self, redis_client: TRedisClient): + async def test_transaction_exec_abort(self, glide_client: TGlideClient): key = get_random_string(10) transaction = BaseTransaction() transaction.custom_command(["INCR", key, key, key]) with pytest.raises(RequestError) as e: - await redis_client.exec(transaction) + await self.exec_transaction(glide_client, transaction) assert "wrong number of arguments" in str( e ) # TODO : add an assert on EXEC ABORT @pytest.mark.parametrize("cluster_mode", [True]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_cluster_transaction(self, redis_client: RedisClusterClient): - assert await redis_client.custom_command(["FLUSHALL"]) == OK + async def test_cluster_transaction(self, glide_client: GlideClusterClient): + assert await glide_client.custom_command(["FLUSHALL"]) == OK keyslot = get_random_string(3) transaction = ClusterTransaction() transaction.info() - expected = await transaction_test(transaction, keyslot, redis_client) - result = await redis_client.exec(transaction) + if await check_if_server_version_lt(glide_client, "7.0.0"): + transaction.publish("test_message", keyslot, False) + else: + transaction.publish("test_message", keyslot, True) + expected = await transaction_test(transaction, keyslot, glide_client) + result = await glide_client.exec(transaction) assert isinstance(result, list) + assert isinstance(result[0], bytes) + result[0] = result[0].decode() assert isinstance(result[0], str) + # Making sure the "info" command is indeed a return at position 0 assert "# Memory" in result[0] - assert result[1:] == expected + assert result[1] == 0 + assert result[2:] == expected @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_can_return_null_on_watch_transaction_failures( - self, redis_client: TRedisClient, request + self, glide_client: TGlideClient, request ): - is_cluster = isinstance(redis_client, RedisClusterClient) + is_cluster = isinstance(glide_client, GlideClusterClient) client2 = await create_client( request, is_cluster, @@ -330,38 +905,81 @@ async def test_can_return_null_on_watch_transaction_failures( keyslot = get_random_string(3) transaction = ClusterTransaction() if is_cluster else Transaction() transaction.get(keyslot) - result1 = await redis_client.custom_command(["WATCH", keyslot]) + result1 = await glide_client.watch([keyslot]) assert result1 == OK result2 = await client2.set(keyslot, "foo") assert result2 == OK - result3 = await redis_client.exec(transaction) + result3 = await self.exec_transaction(glide_client, transaction) assert result3 is None await client2.close() + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_transaction_large_values(self, request, cluster_mode, protocol): + glide_client = await create_client( + request, cluster_mode=cluster_mode, protocol=protocol, timeout=5000 + ) + length = 2**25 # 33mb + key = "0" * length + value = "0" * length + transaction = Transaction() + transaction.set(key, value) + transaction.get(key) + result = await glide_client.exec(transaction) + assert isinstance(result, list) + assert result[0] == OK + assert result[1] == value.encode() + @pytest.mark.parametrize("cluster_mode", [False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_standalone_transaction(self, redis_client: RedisClient): - assert await redis_client.custom_command(["FLUSHALL"]) == OK + async def test_standalone_transaction(self, glide_client: GlideClient): + assert await glide_client.custom_command(["FLUSHALL"]) == OK keyslot = get_random_string(3) - key = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot + key = "{{{}}}:{}".format(keyslot, get_random_string(10)) # to get the same slot + key1 = "{{{}}}:{}".format( + keyslot, get_random_string(10) + ) # to get the same slot value = get_random_string(5) transaction = Transaction() transaction.info() transaction.select(1) + transaction.move(key, 0) transaction.set(key, value) transaction.get(key) + transaction.hset("user:1", {"name": "Alice", "age": "30"}) + transaction.hset("user:2", {"name": "Bob", "age": "25"}) + transaction.lpush(key1, ["2", "1"]) + transaction.sort( + key1, + by_pattern="user:*->age", + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + transaction.sort_store( + key1, + "newSortedKey", + by_pattern="user:*->age", + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) transaction.select(0) transaction.get(key) - expected = await transaction_test(transaction, keyslot, redis_client) - result = await redis_client.exec(transaction) + transaction.publish("test_message", "test_channel") + expected = await transaction_test(transaction, keyslot, glide_client) + result = await glide_client.exec(transaction) assert isinstance(result, list) + assert isinstance(result[0], bytes) + result[0] = result[0].decode() assert isinstance(result[0], str) assert "# Memory" in result[0] - assert result[1:6] == [OK, OK, value, OK, None] - assert result[6:] == expected + assert result[1:5] == [OK, False, OK, value.encode()] + assert result[5:13] == [2, 2, 2, [b"Bob", b"Alice"], 2, OK, None, 0] + assert result[13:] == expected def test_transaction_clear(self): transaction = Transaction() @@ -370,13 +988,207 @@ def test_transaction_clear(self): transaction.clear() assert len(transaction.commands) == 0 + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_standalone_copy_transaction(self, glide_client: GlideClient): + min_version = "6.2.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + keyslot = get_random_string(3) + key = "{{{}}}:{}".format(keyslot, get_random_string(10)) # to get the same slot + key1 = "{{{}}}:{}".format( + keyslot, get_random_string(10) + ) # to get the same slot + value = get_random_string(5) + transaction = Transaction() + transaction.select(1) + transaction.set(key, value) + transaction.copy(key, key1, 1, replace=True) + transaction.get(key1) + result = await glide_client.exec(transaction) + assert result is not None + assert result[2] == True + assert result[3] == value.encode() + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_transaction_chaining_calls(self, redis_client: TRedisClient): - cluster_mode = isinstance(redis_client, RedisClusterClient) + async def test_transaction_chaining_calls(self, glide_client: TGlideClient): + cluster_mode = isinstance(glide_client, GlideClusterClient) key = get_random_string(3) transaction = ClusterTransaction() if cluster_mode else Transaction() transaction.set(key, "value").get(key).delete([key]) - assert await redis_client.exec(transaction) == [OK, "value", 1] + result = await self.exec_transaction(glide_client, transaction) + assert result == [OK, b"value", 1] + + # The object commands are tested here instead of transaction_test because they have special requirements: + # - OBJECT FREQ and OBJECT IDLETIME require specific maxmemory policies to be set on the config + # - we cannot reliably predict the exact response values for OBJECT FREQ, OBJECT IDLETIME, and OBJECT REFCOUNT + # - OBJECT ENCODING is tested here since all the other OBJECT commands are tested here + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_transaction_object_commands( + self, glide_client: TGlideClient, cluster_mode: bool + ): + string_key = get_random_string(10) + maxmemory_policy_key = "maxmemory-policy" + config = await glide_client.config_get([maxmemory_policy_key]) + config_decoded = cast(dict, convert_bytes_to_string_object(config)) + assert config_decoded is not None + maxmemory_policy = cast(str, config_decoded.get(maxmemory_policy_key)) + + try: + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.set(string_key, "foo") + transaction.object_encoding(string_key) + transaction.object_refcount(string_key) + # OBJECT FREQ requires a LFU maxmemory-policy + transaction.config_set({maxmemory_policy_key: "allkeys-lfu"}) + transaction.object_freq(string_key) + # OBJECT IDLETIME requires a non-LFU maxmemory-policy + transaction.config_set({maxmemory_policy_key: "allkeys-random"}) + transaction.object_idletime(string_key) + + response = await self.exec_transaction(glide_client, transaction) + assert response is not None + assert response[0] == OK # transaction.set(string_key, "foo") + assert response[1] == b"embstr" # transaction.object_encoding(string_key) + # transaction.object_refcount(string_key) + assert cast(int, response[2]) >= 0 + # transaction.config_set({maxmemory_policy_key: "allkeys-lfu"}) + assert response[3] == OK + assert cast(int, response[4]) >= 0 # transaction.object_freq(string_key) + # transaction.config_set({maxmemory_policy_key: "allkeys-random"}) + assert response[5] == OK + # transaction.object_idletime(string_key) + assert cast(int, response[6]) >= 0 + finally: + await glide_client.config_set({maxmemory_policy_key: maxmemory_policy}) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_transaction_xinfo_stream( + self, glide_client: TGlideClient, cluster_mode: bool, protocol + ): + key = get_random_string(10) + stream_id1_0 = "1-0" + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.xadd(key, [("foo", "bar")], StreamAddOptions(stream_id1_0)) + transaction.xinfo_stream(key) + transaction.xinfo_stream_full(key) + + response = await self.exec_transaction(glide_client, transaction) + assert response is not None + # transaction.xadd(key, [("foo", "bar")], StreamAddOptions(stream_id1_0)) + assert response[0] == stream_id1_0.encode() + # transaction.xinfo_stream(key) + info = cast(dict, response[1]) + assert info.get(b"length") == 1 + assert info.get(b"groups") == 0 + assert info.get(b"first-entry") == [stream_id1_0.encode(), [b"foo", b"bar"]] + assert info.get(b"first-entry") == info.get(b"last-entry") + + # transaction.xinfo_stream_full(key) + info_full = cast(dict, response[2]) + assert info_full.get(b"length") == 1 + assert info_full.get(b"entries") == [[stream_id1_0.encode(), [b"foo", b"bar"]]] + assert info_full.get(b"groups") == [] + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_transaction_lastsave( + self, glide_client: TGlideClient, cluster_mode: bool + ): + yesterday = date.today() - timedelta(1) + yesterday_unix_time = time.mktime(yesterday.timetuple()) + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.lastsave() + response = await self.exec_transaction(glide_client, transaction) + assert isinstance(response, list) + lastsave_time = response[0] + assert isinstance(lastsave_time, int) + assert lastsave_time > yesterday_unix_time + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lolwut_transaction(self, glide_client: GlideClusterClient): + transaction = ClusterTransaction() + transaction.lolwut().lolwut(5).lolwut(parameters=[1, 2]).lolwut(6, [42]) + results = await glide_client.exec(transaction) + assert results is not None + + for element in results: + assert isinstance(element, bytes) + assert b"Redis ver. " in element + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_transaction_dump_restore( + self, glide_client: TGlideClient, cluster_mode, protocol + ): + cluster_mode = isinstance(glide_client, GlideClusterClient) + keyslot = get_random_string(3) + key1 = "{{{}}}:{}".format( + keyslot, get_random_string(10) + ) # to get the same slot + key2 = "{{{}}}:{}".format(keyslot, get_random_string(10)) + + # Verify Dump + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.set(key1, "value") + transaction.dump(key1) + result1 = await self.exec_transaction(glide_client, transaction) + assert result1 is not None + assert isinstance(result1, list) + assert result1[0] == OK + assert isinstance(result1[1], bytes) + + # Verify Restore - use result1[1] from above + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.restore(key2, 0, result1[1]) + transaction.get(key2) + result2 = await self.exec_transaction(glide_client, transaction) + assert result2 is not None + assert isinstance(result2, list) + assert result2[0] == OK + assert result2[1] == b"value" + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_transaction_function_dump_restore( + self, glide_client: TGlideClient, cluster_mode, protocol + ): + if not await check_if_server_version_lt(glide_client, "7.0.0"): + # Setup (will not verify) + assert await glide_client.function_flush() == OK + lib_name = f"mylib_{get_random_string(10)}" + func_name = f"myfun_{get_random_string(10)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.function_load(code, True) + + # Verify function_dump + transaction.function_dump() + result1 = await self.exec_transaction(glide_client, transaction) + assert result1 is not None + assert isinstance(result1, list) + assert isinstance(result1[1], bytes) + + # Verify function_restore - use result1[2] from above + transaction = ClusterTransaction() if cluster_mode else Transaction() + transaction.function_restore(result1[1], FunctionRestorePolicy.REPLACE) + # For the cluster mode, PRIMARY SlotType is required to avoid the error: + # "RequestError: An error was signalled by the server - + # ReadOnly: You can't write against a read only replica." + result2 = await self.exec_transaction( + glide_client, transaction, SlotIdRoute(SlotType.PRIMARY, 1) + ) + + assert result2 is not None + assert isinstance(result2, list) + assert result2[0] == OK + + # Test clean up + await glide_client.function_flush() diff --git a/python/python/tests/test_utils.py b/python/python/tests/test_utils.py index ca6c75300b..3191db0867 100644 --- a/python/python/tests/test_utils.py +++ b/python/python/tests/test_utils.py @@ -1,7 +1,9 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +import pytest from glide.logger import Level, Logger from tests.conftest import DEFAULT_TEST_LOG_LEVEL +from tests.utils.utils import compare_maps class TestLogger: @@ -16,3 +18,89 @@ def test_set_logger_config(self): # Revert to the tests default log level Logger.set_logger_config(DEFAULT_TEST_LOG_LEVEL) assert Logger.logger_level == DEFAULT_TEST_LOG_LEVEL.value + + +class TestCompareMaps: + def test_empty_maps(self): + map1 = {} + map2 = {} + assert compare_maps(map1, map2) is True + + def test_same_key_value_pairs(self): + map1 = {"a": 1, "b": 2} + map2 = {"a": 1, "b": 2} + assert compare_maps(map1, map2) is True + + def test_different_key_value_pairs(self): + map1 = {"a": 1, "b": 2} + map2 = {"a": 1, "b": 3} + assert compare_maps(map1, map2) is False + + def test_different_key_value_pairs_order(self): + map1 = {"a": 1, "b": 2} + map2 = {"b": 2, "a": 1} + assert compare_maps(map1, map2) is False + + def test_nested_maps_same_values(self): + map1 = {"a": {"b": 1}} + map2 = {"a": {"b": 1}} + assert compare_maps(map1, map2) is True + + def test_nested_maps_different_values(self): + map1 = {"a": {"b": 1}} + map2 = {"a": {"b": 2}} + assert compare_maps(map1, map2) is False + + def test_nested_maps_different_order(self): + map1 = {"a": {"b": 1, "c": 2}} + map2 = {"a": {"c": 2, "b": 1}} + assert compare_maps(map1, map2) is False + + def test_arrays_same_values(self): + map1 = {"a": [1, 2]} + map2 = {"a": [1, 2]} + assert compare_maps(map1, map2) is True + + def test_arrays_different_values(self): + map1 = {"a": [1, 2]} + map2 = {"a": [1, 3]} + assert compare_maps(map1, map2) is False + + def test_null_values(self): + map1 = {"a": None} + map2 = {"a": None} + assert compare_maps(map1, map2) is True + + def test_mixed_types_same_values(self): + map1 = { + "a": 1, + "b": {"c": [2, 3]}, + "d": None, + "e": "string", + "f": [1, "2", True], + } + map2 = { + "a": 1, + "b": {"c": [2, 3]}, + "d": None, + "e": "string", + "f": [1, "2", True], + } + assert compare_maps(map1, map2) is True + + def test_mixed_types_different_values(self): + map1 = { + "a": 1, + "b": {"c": [2, 3]}, + "d": None, + "e": "string", + "f": [1, "2", False], + } + map2 = { + "a": 1, + "b": {"c": [2, 3]}, + "d": None, + "f": [1, "2", False], + "e": "string", + } + assert compare_maps(map1, map2) is False diff --git a/python/python/tests/tests_redis_modules/test_json.py b/python/python/tests/tests_server_modules/test_json.py similarity index 61% rename from python/python/tests/tests_redis_modules/test_json.py rename to python/python/tests/tests_server_modules/test_json.py index e1a4dd381f..c6ee1a72f4 100644 --- a/python/python/tests/tests_redis_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -1,15 +1,15 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import json as OuterJson import pytest from glide.async_commands.core import ConditionalChange, InfoSection -from glide.async_commands.redis_modules import json -from glide.async_commands.redis_modules.json import JsonGetOptions +from glide.async_commands.server_modules import json +from glide.async_commands.server_modules.json import JsonGetOptions from glide.config import ProtocolVersion from glide.constants import OK from glide.exceptions import RequestError -from glide.redis_client import TRedisClient +from glide.glide_client import TGlideClient from tests.test_async_client import get_random_string, parse_info_response @@ -17,37 +17,37 @@ class TestJson: @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_module_is_loaded(self, redis_client: TRedisClient): - res = parse_info_response(await redis_client.info([InfoSection.MODULES])) + async def test_json_module_is_loaded(self, glide_client: TGlideClient): + res = parse_info_response(await glide_client.info([InfoSection.MODULES])) assert "ReJSON" in res["module"] @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_set_get(self, redis_client: TRedisClient): + async def test_json_set_get(self, glide_client: TGlideClient): key = get_random_string(5) json_value = {"a": 1.0, "b": 2} - assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK - result = await json.get(redis_client, key, ".") + result = await json.get(glide_client, key, ".") assert isinstance(result, str) assert OuterJson.loads(result) == json_value - result = await json.get(redis_client, key, ["$.a", "$.b"]) + result = await json.get(glide_client, key, ["$.a", "$.b"]) assert isinstance(result, str) assert OuterJson.loads(result) == {"$.a": [1.0], "$.b": [2]} - assert await json.get(redis_client, "non_existing_key", "$") is None - assert await json.get(redis_client, key, "$.d") == "[]" + assert await json.get(glide_client, "non_existing_key", "$") is None + assert await json.get(glide_client, key, "$.d") == "[]" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_set_get_multiple_values(self, redis_client: TRedisClient): + async def test_json_set_get_multiple_values(self, glide_client: TGlideClient): key = get_random_string(5) assert ( await json.set( - redis_client, + glide_client, key, "$", OuterJson.dumps({"a": {"c": 1, "d": 4}, "b": {"c": 2}, "c": True}), @@ -55,27 +55,27 @@ async def test_json_set_get_multiple_values(self, redis_client: TRedisClient): == OK ) - result = await json.get(redis_client, key, "$..c") + result = await json.get(glide_client, key, "$..c") assert isinstance(result, str) assert OuterJson.loads(result) == [True, 1, 2] - result = await json.get(redis_client, key, ["$..c", "$.c"]) + result = await json.get(glide_client, key, ["$..c", "$.c"]) assert isinstance(result, str) assert OuterJson.loads(result) == {"$..c": [True, 1, 2], "$.c": [True]} - assert await json.set(redis_client, key, "$..c", '"new_value"') == OK - result = await json.get(redis_client, key, "$..c") + assert await json.set(glide_client, key, "$..c", '"new_value"') == OK + result = await json.get(glide_client, key, "$..c") assert isinstance(result, str) assert OuterJson.loads(result) == ["new_value"] * 3 @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_set_conditional_set(self, redis_client: TRedisClient): + async def test_json_set_conditional_set(self, glide_client: TGlideClient): key = get_random_string(5) value = OuterJson.dumps({"a": 1.0, "b": 2}) assert ( await json.set( - redis_client, + glide_client, key, "$", value, @@ -85,7 +85,7 @@ async def test_json_set_conditional_set(self, redis_client: TRedisClient): ) assert ( await json.set( - redis_client, + glide_client, key, "$", value, @@ -96,7 +96,7 @@ async def test_json_set_conditional_set(self, redis_client: TRedisClient): assert ( await json.set( - redis_client, + glide_client, key, "$.a", "4.5", @@ -105,11 +105,11 @@ async def test_json_set_conditional_set(self, redis_client: TRedisClient): is None ) - assert await json.get(redis_client, key, ".a") == "1.0" + assert await json.get(glide_client, key, ".a") == "1.0" assert ( await json.set( - redis_client, + glide_client, key, "$.a", "4.5", @@ -118,15 +118,15 @@ async def test_json_set_conditional_set(self, redis_client: TRedisClient): == OK ) - assert await json.get(redis_client, key, ".a") == "4.5" + assert await json.get(glide_client, key, ".a") == "4.5" @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_get_formatting(self, redis_client: TRedisClient): + async def test_json_get_formatting(self, glide_client: TGlideClient): key = get_random_string(5) assert ( await json.set( - redis_client, + glide_client, key, "$", OuterJson.dumps({"a": 1.0, "b": 2, "c": {"d": 3, "e": 4}}), @@ -135,14 +135,14 @@ async def test_json_get_formatting(self, redis_client: TRedisClient): ) result = await json.get( - redis_client, key, "$", JsonGetOptions(indent=" ", newline="\n", space=" ") + glide_client, key, "$", JsonGetOptions(indent=" ", newline="\n", space=" ") ) expected_result = '[\n {\n "a": 1.0,\n "b": 2,\n "c": {\n "d": 3,\n "e": 4\n }\n }\n]' assert result == expected_result result = await json.get( - redis_client, key, "$", JsonGetOptions(indent="~", newline="\n", space="*") + glide_client, key, "$", JsonGetOptions(indent="~", newline="\n", space="*") ) expected_result = ( @@ -152,55 +152,55 @@ async def test_json_get_formatting(self, redis_client: TRedisClient): @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_del(self, redis_client: TRedisClient): + async def test_del(self, glide_client: TGlideClient): key = get_random_string(5) json_value = {"a": 1.0, "b": {"a": 1, "b": 2.5, "c": True}} - assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK - assert await json.delete(redis_client, key, "$..a") == 2 - assert await json.get(redis_client, key, "$..a") == "[]" + assert await json.delete(glide_client, key, "$..a") == 2 + assert await json.get(glide_client, key, "$..a") == "[]" - result = await json.get(redis_client, key, "$") + result = await json.get(glide_client, key, "$") assert isinstance(result, str) assert OuterJson.loads(result) == [{"b": {"b": 2.5, "c": True}}] - assert await json.delete(redis_client, key, "$") == 1 - assert await json.delete(redis_client, key) == 0 - assert await json.get(redis_client, key, "$") == None + assert await json.delete(glide_client, key, "$") == 1 + assert await json.delete(glide_client, key) == 0 + assert await json.get(glide_client, key, "$") == None @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_forget(self, redis_client: TRedisClient): + async def test_forget(self, glide_client: TGlideClient): key = get_random_string(5) json_value = {"a": 1.0, "b": {"a": 1, "b": 2.5, "c": True}} - assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK - assert await json.forget(redis_client, key, "$..a") == 2 - assert await json.get(redis_client, key, "$..a") == "[]" + assert await json.forget(glide_client, key, "$..a") == 2 + assert await json.get(glide_client, key, "$..a") == "[]" - result = await json.get(redis_client, key, "$") + result = await json.get(glide_client, key, "$") assert isinstance(result, str) assert OuterJson.loads(result) == [{"b": {"b": 2.5, "c": True}}] - assert await json.forget(redis_client, key, "$") == 1 - assert await json.forget(redis_client, key) == 0 - assert await json.get(redis_client, key, "$") == None + assert await json.forget(glide_client, key, "$") == 1 + assert await json.forget(glide_client, key) == 0 + assert await json.get(glide_client, key, "$") == None @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_json_toggle(self, redis_client: TRedisClient): + async def test_json_toggle(self, glide_client: TGlideClient): key = get_random_string(10) json_value = {"bool": True, "nested": {"bool": False, "nested": {"bool": 10}}} - assert await json.set(redis_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK - assert await json.toggle(redis_client, key, "$..bool") == [False, True, None] - assert await json.toggle(redis_client, key, "bool") is True + assert await json.toggle(glide_client, key, "$..bool") == [False, True, None] + assert await json.toggle(glide_client, key, "bool") is True - assert await json.toggle(redis_client, key, "$.nested") == [None] + assert await json.toggle(glide_client, key, "$.nested") == [None] with pytest.raises(RequestError): - assert await json.toggle(redis_client, key, "nested") + assert await json.toggle(glide_client, key, "nested") with pytest.raises(RequestError): - assert await json.toggle(redis_client, "non_exiting_key", "$") + assert await json.toggle(glide_client, "non_exiting_key", "$") diff --git a/python/python/tests/utils/cluster.py b/python/python/tests/utils/cluster.py index ade727f37e..a00ec2d625 100644 --- a/python/python/tests/utils/cluster.py +++ b/python/python/tests/utils/cluster.py @@ -1,9 +1,9 @@ -# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import os import subprocess import sys -from typing import List, Optional +from typing import List, Optional, Union from glide.config import NodeAddress @@ -14,37 +14,41 @@ class RedisCluster: def __init__( self, tls, - cluster_mode: bool, + cluster_mode: bool = False, shard_count: int = 3, replica_count: int = 1, load_module: Optional[List[str]] = None, + addresses: Optional[List[List[str]]] = None, ) -> None: - self.tls = tls - args_list = [sys.executable, SCRIPT_FILE] - if tls: - args_list.append("--tls") - args_list.append("start") - if cluster_mode: - args_list.append("--cluster-mode") - if load_module: - if len(load_module) == 0: - raise ValueError( - "Please provide the path(s) to the module(s) you want to load." - ) - for module in load_module: - args_list.extend(["--load-module", module]) - args_list.append(f"-n {shard_count}") - args_list.append(f"-r {replica_count}") - p = subprocess.Popen( - args_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - output, err = p.communicate(timeout=40) - if p.returncode != 0: - raise Exception(f"Failed to create a cluster. Executed: {p}:\n{err}") - self.parse_cluster_script_start_output(output) + if addresses: + self.init_from_existing_cluster(addresses) + else: + self.tls = tls + args_list = [sys.executable, SCRIPT_FILE] + if tls: + args_list.append("--tls") + args_list.append("start") + if cluster_mode: + args_list.append("--cluster-mode") + if load_module: + if len(load_module) == 0: + raise ValueError( + "Please provide the path(s) to the module(s) you want to load." + ) + for module in load_module: + args_list.extend(["--load-module", module]) + args_list.append(f"-n {shard_count}") + args_list.append(f"-r {replica_count}") + p = subprocess.Popen( + args_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + output, err = p.communicate(timeout=40) + if p.returncode != 0: + raise Exception(f"Failed to create a cluster. Executed: {p}:\n{err}") + self.parse_cluster_script_start_output(output) def parse_cluster_script_start_output(self, output: str): assert "CLUSTER_FOLDER" in output and "CLUSTER_NODES" in output @@ -65,19 +69,26 @@ def parse_cluster_script_start_output(self, output: str): nodes_list.append(NodeAddress(host, int(port))) self.nodes_addr = nodes_list + def init_from_existing_cluster(self, addresses: List[List[str]]): + self.cluster_folder = "" + self.nodes_addr = [] + for [host, port] in addresses: + self.nodes_addr.append(NodeAddress(host, int(port))) + def __del__(self): - args_list = [sys.executable, SCRIPT_FILE] - if self.tls: - args_list.append("--tls") - args_list.extend(["stop", "--cluster-folder", self.cluster_folder]) - p = subprocess.Popen( - args_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - output, err = p.communicate(timeout=20) - if p.returncode != 0: - raise Exception( - f"Failed to stop a cluster {self.cluster_folder}. Executed: {p}:\n{err}" + if self.cluster_folder: + args_list = [sys.executable, SCRIPT_FILE] + if self.tls: + args_list.append("--tls") + args_list.extend(["stop", "--cluster-folder", self.cluster_folder]) + p = subprocess.Popen( + args_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, ) + output, err = p.communicate(timeout=20) + if p.returncode != 0: + raise Exception( + f"Failed to stop a cluster {self.cluster_folder}. Executed: {p}:\n{err}" + ) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py new file mode 100644 index 0000000000..96f08a7b5a --- /dev/null +++ b/python/python/tests/utils/utils.py @@ -0,0 +1,344 @@ +import json +import random +import string +from typing import Any, Dict, List, Mapping, Optional, Set, TypeVar, Union, cast + +import pytest +from glide.async_commands.core import InfoSection +from glide.constants import ( + TClusterResponse, + TFunctionListResponse, + TFunctionStatsResponse, + TResult, +) +from glide.glide_client import TGlideClient +from packaging import version + +T = TypeVar("T") + + +def is_single_response(response: T, single_res: T) -> bool: + """ + Recursively checks if a given response matches the type structure of single_res. + + Args: + response (T): The response to check. + single_res (T): An object with the expected type structure as an example for the single node response. + + Returns: + bool: True if response matches the structure of single_res, False otherwise. + + Example: + >>> is_single_response(["value"], LIST_STR) + True + >>> is_single_response([["value"]], LIST_STR) + False + """ + if isinstance(single_res, list) and isinstance(response, list): + return is_single_response(response[0], single_res[0]) + elif isinstance(response, type(single_res)): + return True + return False + + +def get_first_result( + res: TResult, +) -> bytes: + while isinstance(res, list): + res = ( + res[1] + if not isinstance(res[0], list) and res[0].startswith("127.0.0.1") + else res[0] + ) + + if isinstance(res, dict): + res = list(res.values())[0] + return cast(bytes, res) + + +def parse_info_response(res: Union[bytes, Dict[bytes, bytes]]) -> Dict[str, str]: + res_first = get_first_result(res) + res_decoded = res_first.decode() if isinstance(res_first, bytes) else res_first + info_lines = [ + line for line in res_decoded.splitlines() if line and not line.startswith("#") + ] + info_dict = {} + for line in info_lines: + splitted_line = line.split(":") + key = splitted_line[0] + value = splitted_line[1] + info_dict[key] = value + return info_dict + + +def get_random_string(length): + result_str = "".join(random.choice(string.ascii_letters) for i in range(length)) + return result_str + + +async def check_if_server_version_lt(client: TGlideClient, min_version: str) -> bool: + # TODO: change it to pytest fixture after we'll implement a sync client + info_str = await client.info([InfoSection.SERVER]) + server_version = parse_info_response(info_str).get("redis_version") + assert server_version is not None + return version.parse(server_version) < version.parse(min_version) + + +def compare_maps( + map1: Optional[ + Union[ + Mapping[str, TResult], + Dict[str, TResult], + Mapping[bytes, TResult], + Dict[bytes, TResult], + ] + ], + map2: Optional[ + Union[ + Mapping[str, TResult], + Dict[str, TResult], + Mapping[bytes, TResult], + Dict[bytes, TResult], + ] + ], +) -> bool: + """ + Compare two maps by converting them to JSON strings and checking for equality, including property order. + + Args: + map1 (Optional[Union[Mapping[str, TResult], Dict[str, TResult], Mapping[bytes, TResult], Dict[bytes, TResult]]]): The first map to compare. + map2 (Optional[Union[Mapping[str, TResult], Dict[str, TResult], Mapping[bytes, TResult], Dict[bytes, TResult]]]): The second map to compare. + + Returns: + bool: True if the maps are equal, False otherwise. + + Notes: + This function compares two maps, including their property order. + It checks that each key-value pair in `map1` is equal to the corresponding key-value pair in `map2`, + and ensures that the order of properties is also the same. + Direct comparison with `assert map1 == map2` might ignore the order of properties. + + Example: + mapA = {'name': 'John', 'age': 30} + mapB = {'age': 30, 'name': 'John'} + + # Direct comparison will pass because it ignores property order + assert mapA == mapB # This will pass + + # Correct comparison using compare_maps function + compare_maps(mapA, mapB) # This will return False due to different property order + """ + if map1 is None and map2 is None: + return True + if map1 is None or map2 is None: + return False + return json.dumps(convert_bytes_to_string_object(map1)) == json.dumps( + convert_bytes_to_string_object(map2) + ) + + +def convert_bytes_to_string_object( + # TODO: remove the str options + byte_string_dict: Optional[ + Union[ + List[Any], + Set[bytes], + Mapping[bytes, Any], + Dict[bytes, Any], + Mapping[str, Any], + Dict[str, Any], + ] + ] +) -> Optional[ + Union[ + List[Any], + Set[str], + Mapping[str, Any], + Dict[str, Any], + ] +]: + """ + Recursively convert data structure from byte strings to regular strings, + handling nested data structures of any depth. + """ + if byte_string_dict is None: + return None + + def convert(item: Any) -> Any: + if isinstance(item, dict): + return {convert(key): convert(value) for key, value in item.items()} + elif isinstance(item, list): + return [convert(elem) for elem in item] + elif isinstance(item, set): + return {convert(elem) for elem in item} + elif isinstance(item, bytes): + return item.decode("utf-8") + else: + return item + + return convert(byte_string_dict) + + +def convert_string_to_bytes_object( + string_structure: Optional[ + Union[ + List[Any], + Set[str], + Mapping[str, Any], + Dict[str, Any], + ] + ] +) -> Optional[ + Union[ + List[Any], + Set[bytes], + Mapping[bytes, Any], + Dict[bytes, Any], + ] +]: + """ + Recursively convert the data structure from strings to bytes, + handling nested data structures of any depth. + """ + if string_structure is None: + return None + + def convert(item: Any) -> Any: + if isinstance(item, dict): + return {convert(key): convert(value) for key, value in item.items()} + elif isinstance(item, list): + return [convert(elem) for elem in item] + elif isinstance(item, set): + return {convert(elem) for elem in item} + elif isinstance(item, str): + return item.encode("utf-8") + else: + return item + + return convert(string_structure) + + +def generate_lua_lib_code( + lib_name: str, functions: Mapping[str, str], readonly: bool +) -> str: + code = f"#!lua name={lib_name}\n" + for function_name, function_body in functions.items(): + code += ( + f"redis.register_function{{ function_name = '{function_name}', callback = function(keys, args) " + f"{function_body} end" + ) + if readonly: + code += ", flags = { 'no-writes' }" + code += " }\n" + return code + + +def create_lua_lib_with_long_running_function( + lib_name: str, func_name: str, timeout: int, readonly: bool +) -> str: + """ + Create a lua lib with a (optionally) RO function which runs an endless loop up to timeout sec. + Execution takes at least 5 sec regardless of the timeout configured. + """ + code = ( + f"#!lua name={lib_name}\n" + f"local function {lib_name}_{func_name}(keys, args)\n" + " local started = tonumber(redis.pcall('time')[1])\n" + # fun fact - redis does no writes if 'no-writes' flag is set + " redis.pcall('set', keys[1], 42)\n" + " while (true) do\n" + " local now = tonumber(redis.pcall('time')[1])\n" + f" if now > started + {timeout} then\n" + f" return 'Timed out {timeout} sec'\n" + " end\n" + " end\n" + " return 'OK'\n" + "end\n" + "redis.register_function{\n" + f"function_name='{func_name}',\n" + f"callback={lib_name}_{func_name},\n" + ) + if readonly: + code += "flags={ 'no-writes' }\n" + code += "}" + return code + + +def check_function_list_response( + response: TClusterResponse[TFunctionListResponse], + lib_name: str, + function_descriptions: Mapping[str, Optional[bytes]], + function_flags: Mapping[str, Set[bytes]], + lib_code: Optional[str] = None, +): + """ + Validate whether `FUNCTION LIST` response contains required info. + + Args: + response (List[Mapping[bytes, Any]]): The response from redis. + libName (bytes): Expected library name. + functionDescriptions (Mapping[bytes, Optional[bytes]]): Expected function descriptions. Key - function name, value - + description. + functionFlags (Mapping[bytes, Set[bytes]]): Expected function flags. Key - function name, value - flags set. + libCode (Optional[bytes]): Expected library to check if given. + """ + response = cast(TFunctionListResponse, response) + assert len(response) > 0 + has_lib = False + for lib in response: + has_lib = lib.get(b"library_name") == lib_name.encode() + if has_lib: + functions: List[Mapping[bytes, Any]] = cast( + List[Mapping[bytes, Any]], lib.get(b"functions") + ) + assert len(functions) == len(function_descriptions) + for function in functions: + function_name: bytes = cast(bytes, function.get(b"name")) + assert function.get(b"description") == function_descriptions.get( + function_name.decode("utf-8") + ) + assert function.get(b"flags") == function_flags.get( + function_name.decode("utf-8") + ) + + if lib_code: + assert lib.get(b"library_code") == lib_code.encode() + break + + assert has_lib is True + + +def check_function_stats_response( + response: TFunctionStatsResponse, + running_function: List[bytes], + lib_count: int, + function_count: int, +): + """ + Validate whether `FUNCTION STATS` response contains required info. + + Args: + response (TFunctionStatsResponse): The response from server. + running_function (List[bytes]): Command line of running function expected. Empty, if nothing expected. + lib_count (int): Expected libraries count. + function_count (int): Expected functions count. + """ + running_script_info = response.get(b"running_script") + if running_script_info is None and len(running_function) != 0: + pytest.fail("No running function info") + + if running_script_info is not None and len(running_function) == 0: + command = cast(dict, running_script_info).get(b"command") + pytest.fail("Unexpected running function info: " + " ".join(cast(str, command))) + + if running_script_info is not None: + command = cast(dict, running_script_info).get(b"command") + assert running_function == command + # command line format is: + # fcall|fcall_ro * * + assert running_function[1] == cast(dict, running_script_info).get(b"name") + + expected = { + b"LUA": {b"libraries_count": lib_count, b"functions_count": function_count} + } + assert expected == response.get(b"engines") diff --git a/python/src/lib.rs b/python/src/lib.rs index 3b07eb93c3..b37ff1def8 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,16 +1,19 @@ +use bytes::Bytes; +use glide_core::client::FINISHED_SCAN_CURSOR; /** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use glide_core::start_socket_listener; -use pyo3::exceptions::PyUnicodeDecodeError; +use glide_core::MAX_REQUEST_ARGS_LENGTH; +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; -use pyo3::types::{PyBool, PyDict, PyFloat, PyList, PySet}; +use pyo3::types::{PyAny, PyBool, PyBytes, PyDict, PyFloat, PyList, PySet}; use pyo3::Python; - use redis::Value; pub const DEFAULT_TIMEOUT_IN_MILLISECONDS: u32 = glide_core::client::DEFAULT_RESPONSE_TIMEOUT.as_millis() as u32; +pub const MAX_REQUEST_ARGS_LEN: u32 = MAX_REQUEST_ARGS_LENGTH as u32; #[pyclass] #[derive(PartialEq, Eq, PartialOrd, Clone)] @@ -30,6 +33,42 @@ impl Level { } } +/// This struct is used to keep track of the cursor of a cluster scan. +/// We want to avoid passing the cursor between layers of the application, +/// So we keep the state in the container and only pass the id of the cursor. +/// The cursor is stored in the container and can be retrieved using the id. +/// The cursor is removed from the container when the object is deleted (dropped). +#[pyclass] +#[derive(Default)] +pub struct ClusterScanCursor { + cursor: String, +} + +#[pymethods] +impl ClusterScanCursor { + #[new] + fn new(new_cursor: Option) -> Self { + match new_cursor { + Some(cursor) => ClusterScanCursor { cursor }, + None => ClusterScanCursor::default(), + } + } + + fn get_cursor(&self) -> String { + self.cursor.clone() + } + + fn is_finished(&self) -> bool { + self.cursor == *FINISHED_SCAN_CURSOR.to_string() + } +} + +impl Drop for ClusterScanCursor { + fn drop(&mut self) { + glide_core::cluster_scan_container::remove_scan_state_cursor(self.cursor.clone()); + } +} + #[pyclass] pub struct Script { hash: String, @@ -38,9 +77,18 @@ pub struct Script { #[pymethods] impl Script { #[new] - fn new(code: String) -> Self { - let hash = glide_core::scripts_container::add_script(&code); - Script { hash } + fn new(code: &PyAny) -> PyResult { + let hash = if let Ok(code_str) = code.extract::() { + glide_core::scripts_container::add_script(code_str.as_bytes()) + } else if let Ok(code_bytes) = code.extract::<&PyBytes>() { + glide_core::scripts_container::add_script(code_bytes.as_bytes()) + } else { + return Err(PyTypeError::new_err( + "code must be either a String or PyBytes", + )); + }; + + Ok(Script { hash }) } fn get_hash(&self) -> String { @@ -57,10 +105,12 @@ impl Script { fn glide(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::